Session Inactivity Timeout with Dojo

By Chris L Hardin
Sr. Software Architect

A common problem is Ajax applications is what to do with the user when their session times out. Everyone handles this one differently, so there isn’t really a right or wrong way to do it. It depends on the business cases that you have how you will implement it. I decided to share the way I have done it for an application I am working on.

The first thing you need is a custom object that you will use to create the timeout and publish the idle and active events that we will subscribe to later. This is a copy of a class that I found on the Dojo Website, but it’s been heavily modified to make it work correctly.

dojo.declare("Monitor", null ,{
_events : [[window, 'scroll'], [window, 'resize'], [document, 'mousemove'], [document, 'keydown']],
_idleTime : null,
_timers : null,
idleTime : null,

constructor: function(time,timers){
Monitor.prototype.time=time;
this._timers=timers;
this.initObservers();
this.setTimer();
},
initObservers:function(){
dojo.forEach(this._events, function(e){
dojo.connect(e[0],e[1], function(event){
Monitor.prototype.onInterrupt();
});
})
},
onInterrupt:function(){
this.idleTime = new Date() - this._idleTime;
dojo.publish("state:active", [this.idleTime]);
this.setTimer();
},
setTimer: function(){
var oj = Monitor.prototype;
this.clearTimers();
this._idleTime = new Date();

this._timers.push(setTimeout(function(){
dojo.publish("state:idle", null);
},oj.time));

},
clearTimers: function(){
if(this._timers){
for (var i= 0;i < this._timers.length; i++) { console.debug("Clearing Timer: " + this._timers[i]); clearTimeout(this._timers[i]); } } this._timers = new Array(); } });

Now you need to load the timer up when the page loads. We store the ids for each timer in a global variable. This makes darn sure we wipe out all timers when an event triggers the clear.


var timerArray = new Array();

dojo.addOnLoad(function(){
var timeout = 840000; //Timeout is 15 minutes //900 000 15 minutes
var monitor = new Monitor(timeout,timerArray);

dojo.subscribe("state:active", null, onActive);
dojo.subscribe("state:idle", null, onIdle);

function onActive(args){
// dojo.query("p").style("opacity",1);
//console.debug("active... after: " + args + "ms");
monitor.clearTimers();

};
function onIdle(){
//dojo.query("p").style("opacity",0.2);

console.debug("idle...logging out");
var logoutTimeout = setTimeout(logoutNow, 60000); //Show the logout dialog for 1 minute. Give time to reset.
raiseQueryDialog("Session Timeout", "Your session is about to timeout. Would you like to continue?", function(isContinue){

if (isContinue){
clearTimeout(logoutTimeout);
monitor.clearTimers();

} else {

logoutNow();
}

});

//window.location.href='j_spring_security_logout?inactive=y';

};
});

var logoutNow = function(){

window.location.href='j_spring_security_logout?inactive=y';

}

var randomString = function randomString() {
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
var string_length = 8;
var randomstring = '';
for (var i=0; i%lt;string_length; i++) {
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum,rnum+1);
}
return randomstring;
}

function raiseQueryDialog(title, question, callbackFn, e) {

var randomId = randomString();

var errorDialog = new dj.Dialog({ id: randomId, title: title });
// When either button is pressed, kill the dialog and call the callbackFn.
var commonCallback = function(mouseEvent) {
errorDialog.hide();
errorDialog.destroyRecursive();
// alert(mouseEvent.explicitOriginalTarget.nodeName);

if (window.event) e = window.event;

var srcEl = mouseEvent.srcElement? mouseEvent.srcElement : mouseEvent.target;

if (srcEl.id == 'yesButton' +randomId) {
callbackFn(true, e);
} else {
callbackFn(false, e);
}
};
var questionDiv = dojo.create('div', { innerHTML: question });
var yesButton = new dj.form.Button(
{ label: 'Yes', id: 'yesButton' +randomId, onClick: commonCallback });
var noButton = new dj.form.Button(
{ label: 'No', id: 'noButton' +randomId, onClick: commonCallback });

errorDialog.containerNode.appendChild(questionDiv);
errorDialog.containerNode.appendChild(yesButton.domNode);
errorDialog.containerNode.appendChild(noButton.domNode);
errorDialog.show();
}

What happens in this example is that after 14 minutes of idle, a message will popup asking the user if they would like to continue. If the user chooses to continue, then the timer is reset, if no, then they get logged out. If they aren’t around to confirm anything, the app will automatically logout after 1 minute from displaying the message.

Spring Security 3.0 with Active Directory LDAP redux…plus remember me functionality

A while back, I released my spring security configuration for doing Spring Security with Active Directory. I have since made a few changes to my configuration that has simplified it and I also got the remember me functionality working. For those of you who don’t know, remember me is a way to tell the application to remember a user for a given length of time and expire when that time is up to make them relogin.

The remember me functionality came in real handy for us since we have so much Ajax going on. It was a pain for users when they would time out after 15 minutes and immediately get redirected to the login page. I also made my configuration utilize a database to store the saved user data instead of allowing it to create everything in a cookie on the user’s machine which is extremely un-secure. I use this technique in two applications. One of them uses an in-memory database mapped with hibernate and the other uses a physical Oracle database. In order to make the PersistentToken storage for Spring 3.0 work, you need to create a table. Below the configuration is a hibernate class you can use to automatically create the table for you. I mapped the table with hibernate so that I could create a screen that tracks all logged in users, when they logged in, etc. It also gives me the ability to play God and log people out just by deleting their user record.

Another reason I chose the Persistent token mechanism is that I couldn’t get the plain token mechanism to work.

Well enough of my chatter and let’s look at my updated Active Directory and LDAP configuration with Spring 3.0. Take note that this configuration is a hybrid configuration between namespace and bean configurations. The key elements to making this configuration sing are in bold.

<beans xmlns="http://www.springframework.org/schema/beans&quot;
xmlns:context=”http://www.springframework.org/schema/context&#8221;
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance&#8221; xmlns:aop=”http://www.springframework.org/schema/aop&#8221;
xmlns:security=”http://www.springframework.org/schema/security&#8221;
xmlns:tx=”http://www.springframework.org/schema/tx&#8221;
xsi:schemaLocation=”
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd”&gt;

<security:global-method-security
secured-annotations=”enabled” jsr250-annotations=”enabled”
pre-post-annotations=”enabled” />

<security:http use-expressions=”true” auto-config=’false’
realm=”project” entry-point-ref=”authenticationProcessingFilterEntryPoint”>

<security:intercept-url pattern="/login.jsp*"
filters=”none” />

<security:intercept-url pattern="/logout.jsp*"
filters=”none” />

<security:form-login login-page='/login.jsp'
default-target-url=’/index.jsp’ always-use-default-target=’true’ />

<!–
<security:intercept-url pattern="/*.jsp"
access=”hasRole(‘ROLE_WEB_PROMISES_INQUIRY’)” />
–>

<security:intercept-url pattern="/portal.jsp"
access=”hasAnyRole(‘ROLE_USER’,’ROLE_iArchiveViewer_Backoffice’,’ROLE_SUPPORT’)” />

<security:intercept-url pattern="/"
access=”hasAnyRole(‘ROLE_USER’,’ROLE_iArchiveViewer_Backoffice’,’ROLE_SUPPORT’)” />

<security:intercept-url pattern="/index.jsp"
access=”hasAnyRole(‘ROLE_USER’,’ROLE_iArchiveViewer_Backoffice’,’ROLE_SUPPORT’)” />

<security:intercept-url pattern="/rest/**"
access=”hasAnyRole(‘ROLE_USER’,’ROLE_iArchiveViewer_Backoffice’,’ROLE_SUPPORT’)” />

<security:intercept-url pattern="/RPCAdapter/**"
access=”hasAnyRole(‘ROLE_USER’,’ROLE_iArchiveViewer_Backoffice’,’ROLE_SUPPORT’)” />

<!–
<security:intercept-url pattern="/rest/transaction/payment/cancel/**"
access=”hasRole(‘ROLE_WEB_PROMISES_INQUIRY’)” />
–>

<!– –>

<!–
<concurrent-session-control max-sessions="1"
exception-if-maximum-exceeded=”true”/>
–>

<!– –>

<!– If you want to use basic authentication–>

<!–
Used if access is denied to the application. errorPage = the page to
display if access is denied.
–>
<bean id="accessDeniedHandler"
class=”org.springframework.security.web.access.AccessDeniedHandlerImpl”>

<bean id="authenticationProcessingFilterEntryPoint"
class=”org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint”>

<!–
============================== LDAP Definitions
======================================
–>

<!–
constructor = LDAP domain name userDn = user id used to query the
directory password = password for managerDn baseEnvironmentProperties
= additional ldap environment properties
–>
<bean id="initialDirContextFactory"
class=”org.springframework.security.ldap.DefaultSpringSecurityContextSource”>
<constructor-arg
value=”ldap://domain:389/dc=domain,dc=com” />
<property name="userDn"
value=”CN=projectldap,OU=Service Accounts,OU=Organizational Units,DC=domain,DC=com” />

follow

<!–
Used to search active directory for the user id of the user attempting
to login to the system. 1. Instead of binding directly, Acegi uses the
filter to find a matching user 2. If no user is found, that’s a
failure. 3. If a user is found, takes that users DN and tries to bind
using it 4. Success to bind means that we are okay, failure means
incorrect password constructor 0 = BaseDN for user search constructor
1 = Filter statement for user id lookup constructor 2 = initial
context factory defined above searchSubtree = search the subtrees
beneath BaseDN.
–>
<bean id="userSearch"
class=”org.springframework.security.ldap.search.FilterBasedLdapUserSearch”>

<!–
Peform the LDAP user bind using the context factory and user search

constructor = initial context factory defined above userSearch =
userSearch defined above
–>
<bean id="bindAuthenticator"
class=”org.springframework.security.ldap.authentication.BindAuthenticator”>

<!–
Used for authorization. Populate the user with a set of roles based
upon AD groups. constructor 0 = context factory constructor 1 = BaseDN
for groups groupRoleAttribute = attribute used for role names
–>
<bean id="authoritiesPopulator"
class=”org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator”>

<!–
************************** Athentication
************************************
–>

<!–
LDAP Authentication Provider uses the bindAuthenticator and
authoritiesPopulator to authenticate the user and populate the user
object with roles based upon Active Directory groups.
–>
<bean id="ldapAuthProvider"
class=”org.springframework.security.ldap.authentication.LdapAuthenticationProvider”>

<bean id="userDetailsContextMapper"
class=”com.domain.acegi.AttributesLDAPUserDetailsContextMapper”>

description

ROLE_USER
ROLE_iArchiveViewer_Backoffice
ROLE_SUPPORT

<bean id="userDetailsService"
class=”org.springframework.security.ldap.userdetails.LdapUserDetailsService”>

<security:authentication-provider
ref=”ldapAuthProvider” user-service-ref=”userService” />

Here is the Hibernate class I use to create the table for the Persistent token mechanism.

import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import com.bbvacompass.domain.DataObject;

@Entity
@Table(name="PERSISTENT_LOGINS")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@org.hibernate.annotations.Entity(mutable = false)
public class PersistentLogin extends DataObject implements java.io.Serializable{

private Integer id;
private String userName;

private String series;//Primary Key
private String token;
private Timestamp lastUsed;

@Override
@Column(name="ID")
public Integer getId() {

return id;
}

@Override
public void setId(Integer id) {
this.id = id;

}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

@Id
public String getSeries() {
return series;
}

public void setSeries(String series) {
this.series = series;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}

@Column(name="LAST_USED")
public Timestamp getLastUsed() {
return lastUsed;
}

public void setLastUsed(Timestamp lastUsed) {
this.lastUsed = lastUsed;
}

}

I almost forgot one important step. You need to pass the correct paramter when a user logs in to trigger the remember me. I use a hidden field, because we didn’t want users choosing the option. It will be automatic. Remember the parameter must be _spring_security_remember_me=on or it will not work. The following code is in the login form I use.

Also, don’t forget to provide a logout button somewhere or the user will never be able to log out until the cookie expires and the above configuration is for two weeks.

Spring 3.0 – Mapping Active Directory LDAP custom attributes to UserDetails

In earlier versions of Spring Security/Acegi, the UserDetails object contained a method called getAttributes(). This method would return the custom attributes assigned to a user. In the case of Active Directory, this contained all the little minutia about a user that a domain defines, a good example in my case was that it contained personal information about a user’s work location that I needed to access.

The getAttributes method was deprecated and removed in Spring 3.0, leaving no way to access these attributes. The documentation states that now in order to get at these attributes you need to define your own implementation of a UserDetailsMapper and populate your own implementation of a UserDetails object. I was really shocked that this functionality was completely removed, but I two good reasons I can think of is that the data is stored differently in other LDAP implementations and the attribute list could get quite large and since it gets carried around in memory and if you have several hundred users logging in…well you can see the memory problem.

I wrote my own implementation for this in hopes that it might help others. There isn’t any other implementation out there I could find so I was forced to write it from scratch.

First thing, we need to define this in our spring security setup.

<bean id="ldapAuthProvider"
class=”org.springframework.security.ldap.authentication.LdapAuthenticationProvider”>

<bean id="userDetailsContextMapper"
class=”com.domain.project.security.AttributesLDAPUserDetailsContextMapper”>

description

ROLE_Teller-Users
ROLE_iArchiveViewer_Backoffice
ROLE_Navigator Support

Note the sections in bold are the key sections you may or may not want to change. let’s look at our mapping class now, AttributesLDAPUserDetailsContextMapper.

import java.util.List;

import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ldap.core.DirContextAdapter;

import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.util.Assert;

public class AttributesLDAPUserDetailsContextMapper extends
LdapUserDetailsMapper {

private Logger logger = LoggerFactory
.getLogger(AttributesLDAPUserDetailsContextMapper.class);

private String[] attributesToPopulate = new String[] {};
private String[] roleAttributes = null;
private boolean mapAllAttributes = false;

public boolean isMapAllAttributes() {
return mapAllAttributes;
}

public void setMapAllAttributes(boolean mapAllAttributes) {
this.mapAllAttributes = mapAllAttributes;
}

public String[] getAttributesToPopulate() {
return attributesToPopulate;
}

public void setAttributesToPopulate(String[] attributesToPopulate) {
this.attributesToPopulate = attributesToPopulate;
}

@Override
public UserDetails mapUserFromContext(DirContextOperations ctx,
String username, List authorities) {

// LdapUserDetailsImpl origImpl =
// (LdapUserDetailsImpl)super.mapUserFromContext(ctx, username,
// authorities);

String dn = ctx.getNameInNamespace();

logger.debug("Mapping user details from context with DN: " + dn);

ExtUserDetails.Essence essence = new ExtUserDetails.Essence();
essence.setDn(dn);

// Object passwordValue =
// ctx.getObjectAttribute(super.passwordAttributeName);

// if (passwordValue != null) {
// essence.setPassword(mapPassword(passwordValue));
// }

essence.setUsername(username);

// Map the roles
for (int i = 0; (roleAttributes != null) && (i < roleAttributes.length); i++) { String[] rolesForAttribute = ctx .getStringAttributes(roleAttributes[i]); if (rolesForAttribute == null) { logger.debug("Couldn't read role attribute '" + roleAttributes[i] + "' for user " + dn); continue; } for (int j = 0; j < rolesForAttribute.length; j++) { GrantedAuthority authority = createAuthority(rolesForAttribute[j]); if (authority != null) { essence.addAuthority(authority); } } } // Add the supplied authorities for (GrantedAuthority grantedAuthority : authorities) { essence.addAuthority(grantedAuthority); } DirContextAdapter dca = (DirContextAdapter) ctx;

for (int i = 0; i < attributesToPopulate.length; i++) { String attribute = dca.getStringAttribute(attributesToPopulate[i]); essence.addAttribute(attributesToPopulate[i], attribute); logger.debug("Custom Attribute: '" + attributesToPopulate[i] + "' =" + attribute); } if (isMapAllAttributes()) {

Attributes attributes = dca.getAttributes();

NamingEnumeration attributelist = attributes.getIDs();

while (attributelist.hasMoreElements()) {
String key = (String) attributelist.nextElement();

essence.addAttribute(key, attributes.get(key));
logger.debug("Attribute List: '" + key + "' ="
+ attributes.get(key));
}

}

return essence.createUserDetails();

}

@Override
public void setRoleAttributes(String[] roleAttributes) {
Assert.notNull(roleAttributes, "roleAttributes array cannot be null");
this.roleAttributes = roleAttributes;
}

}

There is some overlap here. I set mapAllAttributes to true in the xml, so it will place all of the LDAP attributes in the HashMap inside of UserDetails and I also could have set this to false and just mapped certains ones that I want to load instead of loading the entire list.

Now we need our new UserDetailsClass. This class is a carbon copy of LdapUserDetailsImpl with a few changes.


import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;

import javax.naming.Name;

import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.ldap.ppolicy.PasswordPolicyData;
import org.springframework.util.Assert;

import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;

public class ExtUserDetails extends LdapUserDetailsImpl {

//~ Instance fields ================================================================================================

private String dn;
private String password;
private String username;
private List authorities = AuthorityUtils.NO_AUTHORITIES;
private Map attributes = new HashMap();

private boolean accountNonExpired = true;
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = true;
private boolean enabled = true;
// PPolicy data
private int timeBeforeExpiration = Integer.MAX_VALUE;
private int graceLoginsRemaining = Integer.MAX_VALUE;

//~ Constructors ===================================================================================================

protected ExtUserDetails() {}

//~ Methods ========================================================================================================
public Map getAttributes() {
return attributes;
}

public Object getAttribute(String key){

return attributes.get(key);
}

public List getAuthorities() {
return authorities;
}

public String getDn() {
return dn;
}

public String getPassword() {
return password;
}

public String getUsername() {
return username;
}

public boolean isAccountNonExpired() {
return accountNonExpired;
}

public boolean isAccountNonLocked() {
return accountNonLocked;
}

public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}

public boolean isEnabled() {
return enabled;
}

public int getTimeBeforeExpiration() {
return timeBeforeExpiration;
}

public int getGraceLoginsRemaining() {
return graceLoginsRemaining;
}

public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(super.toString()).append(": ");
sb.append("Username: ").append(this.username).append("; ");
sb.append("Password: [PROTECTED]; ");
sb.append("Enabled: ").append(this.enabled).append("; ");
sb.append("AccountNonExpired: ").append(this.accountNonExpired).append("; ");
sb.append("credentialsNonExpired: ").append(this.credentialsNonExpired).append("; ");
sb.append("AccountNonLocked: ").append(this.accountNonLocked).append("; ");

if (this.getAuthorities() != null) {
sb.append("Granted Authorities: ");

for (int i = 0; i 0) {
sb.append(", ");
}

sb.append(this.getAuthorities().get(i).toString());
}
} else {
sb.append("Not granted any authorities");
}

return sb.toString();
}

//~ Inner Classes ==================================================================================================

/**
* Variation of essence pattern. Used to create mutable intermediate object
*/
public static class Essence {
protected ExtUserDetails instance = createTarget();
private List mutableAuthorities = new ArrayList();
private Map mutableAttributes = new HashMap();

public Essence() { }

public Essence(DirContextOperations ctx) {
setDn(ctx.getDn());
}

public Essence(ExtUserDetails copyMe) {
setDn(copyMe.getDn());
setUsername(copyMe.getUsername());
setPassword(copyMe.getPassword());
setEnabled(copyMe.isEnabled());
setAccountNonExpired(copyMe.isAccountNonExpired());
setCredentialsNonExpired(copyMe.isCredentialsNonExpired());
setAccountNonLocked(copyMe.isAccountNonLocked());
setAuthorities(copyMe.getAuthorities());
setAttributes(copyMe.getAttributes());
}

protected ExtUserDetails createTarget() {
return new ExtUserDetails();
}

/** Adds the authority to the list, unless it is already there, in which case it is ignored */
public void addAttribute(String key, Object value) {
mutableAttributes.put(key, value);
}

/** Adds the authority to the list, unless it is already there, in which case it is ignored */
public void addAuthority(GrantedAuthority a) {
if(!hasAuthority(a)) {
mutableAuthorities.add(a);
}
}

private boolean hasAuthority(GrantedAuthority a) {
for (GrantedAuthority authority : mutableAuthorities) {
if(authority.equals(a)) {
return true;
}
}
return false;
}

public ExtUserDetails createUserDetails() {
Assert.notNull(instance, "Essence can only be used to create a single instance");
Assert.notNull(instance.username, "username must not be null");
Assert.notNull(instance.getDn(), "Distinguished name must not be null");

instance.authorities = getGrantedAuthorities();

ExtUserDetails newInstance = instance;

instance = null;

return newInstance;
}

public List getGrantedAuthorities() {
return mutableAuthorities;
}

public void setAccountNonExpired(boolean accountNonExpired) {
instance.accountNonExpired = accountNonExpired;
}

public void setAccountNonLocked(boolean accountNonLocked) {
instance.accountNonLocked = accountNonLocked;
}

public void setAuthorities(List authorities) {
mutableAuthorities = authorities;
}

public void setAttributes(Map attributes) {
mutableAttributes = attributes;
}

public void setCredentialsNonExpired(boolean credentialsNonExpired) {
instance.credentialsNonExpired = credentialsNonExpired;
}

public void setDn(String dn) {
instance.dn = dn;
}

public void setDn(Name dn) {
instance.dn = dn.toString();
}

public void setEnabled(boolean enabled) {
instance.enabled = enabled;
}

public void setPassword(String password) {
instance.password = password;
}

public void setUsername(String username) {
instance.username = username;
}

public void setTimeBeforeExpiration(int timeBeforeExpiration) {
instance.timeBeforeExpiration = timeBeforeExpiration;
}

public void setGraceLoginsRemaining(int graceLoginsRemaining) {
instance.graceLoginsRemaining = graceLoginsRemaining;
}
}
}

This class contains our hashmap where our custom attributes are stored.

We have all of our pieces, now we just need to access the attributes.

Object o = SecurityContextHolder.getContext().getAuthentication().getDetails();

if (o instanceof ExtUserDetails){
return (String) ((ExtUserDetails) o).getAttributes().get(“description”);
} else {
return o.toString();
}

We just cast the UserDetails object back to our instance so we can access the values it is holding for us.

For my taste, this is too many steps to providing a UserDetails mapping strategy. spring needs a better mechanism or it at least needs to include my methods above as an option for folks to use. A default strategy is better than none at all.

Dojo Week: Populate a grid from a store as you scroll

By Chris L Hardin
Sr. Software Architect
Have you ever wanted to do away with the paging aspect of your grids. Chances are you have grid that have page and you click next and back and all that jazz, but I want to give you something better… a grid that populates data as you scroll along.

Let’s get right to the example using Dojo. First you need to create a Grid in your page.

Your going to need to define the layout of your grid using Dojo. This layout is simple. It only contains two columns.

var gridLayout = [{
defaultCell: { width: 8, editable: false, type: dojox.grid.cells._Widget, styles: 'text-align: left;' },
rows : [
{
name : 'ID',
field : 'id',
width : '120px',
hidden : true
},
{
name : 'Name',
field : 'name',
width : '120px'
}
}];

Now we’ll create our Grid definition so that we can tie into some events and do some custom processing.


// Checking / Savings Grid
d.declare("MyGrid", dojox.grid.DataGrid, {
loading: function(){
//d.style(this.domNode, "opacity", .5);
},
loaded: function(){
//d.style(this.domNode, "opacity", 1);
},
onStyleRow: function (inRow){
var item = this.getItem( inRow.index );
if( item ){
//inRow.customStyles += ' cursor: url(./image/finger.cur), pointer; ';

if (item.name && item.name== 'Bob'){

inRow.customStyles +=' background-color: #FFCCFF; ';

}

if (item.alertind && item.alertind == 'Jane'){

inRow.customStyles +=' background-color: #FFF072; ';

}

inRow.customStyles += ' cursor: pointer; ';

}
this.inherited(arguments);
},
startup: function(){
this.inherited(arguments);
},
postrender: function(){

this.inherited(arguments);
},
renderRow: function(inRowIndex, inNodes){
console.debug("ChkSavGrid renderRow called . . . ");
this.inherited(arguments);
},
onRowClick: function(e){
this.loading();
var item = this.getItem(e.rowIndex);

return this.inherited(arguments);
},
onRowDblClick: function(e){
return this.inherited(arguments);
}
});

//Now let’s add our Grid to an HTML page.

<div id="myGrid" jsId="myGridId" dojoType="MyGrid" rowSelector="20px" structure="gridLayout"
style=”width: 870px; height: 150px” rowsPerPage=”10″ escapeHTMLInData=”false”>

We set Rows per page to 10, this means that after you scroll closer to the 10th row,Dojo needs to go back to the server to get more data.

Now we need to create a QueryReadStore to get our data. I amusing Spring to convert my objects to JSON and the object returned is always a QueryReadStore result that I define. Below is how the returned json looks and how I extended QueryReadStore to get the result properly. Note that the elements in the JSON data are required to be in this structure for the QueryReadStore to work property with the Grid.

{“queryReadStoreResult”:{“items”:[{“id”:1, name: “Bob”}],”identifier”:”id”,”numRows”:618}

I took out all rows in items but one to save space, but there are 618 people in the database and 10 would be returned initially.

d.declare(“ModifiedQueryReadStore”, dojox.data.QueryReadStore, {
_xhrFetchHandler: function(data, request, fetchHandler, errorHandler){
return dojox.data.QueryReadStore.prototype._xhrFetchHandler.apply(this,
[data.queryReadStoreResult, request,fetchHandler, errorHandler]);
}
});

Now we need to create out store and populate our grid for the first run.


//You can call this method by dojo.addOnLoad

var loadGrid = function(){

var thisGrid = dj.byId("myGrid");
var url = "rest/people";
var query = {name: "bob"};

var params = {
url: url,
urlPreventCache: true,
start: 0,
count: 10,
serverQuery: query,
query: query,
requestMethod:"post",
queryOptions:{ignoreCase:false},
// onBegin: dojo.hitch(this, "beginReturn"),
onComplete: function(e){
console.info("Data Loaded!");

},
onError: function(error){
console.error(error);

}
}

var thisStore = new ModifiedQueryReadStore(params);
thisGrid.setStore(thisStore, query, {ignoreCase:false});

}

Dojo will send the parameter of name=Bob to the server and also a start=0 and count=10. As you scroll, these parameters will get updated and resent until the total number of rows are loaded. Really cool eh?

Here are some final thoughts and concerns I have about this.

1. The Grid needs to work in reverse as well. This grid will continue to display rows until all are loaded in memory. It needs to remove older rows as you pass them and ifyou scroll back up, go back to the server an reget them. This should be done via some type of setting on the Grid or Store.

2. There needs to be some paramter on the store itself to tell the store what object contains the items, this way I wouldn’t need to extend QueryReadStore.

3. I would prefer that instead of post parameters,the QueryReadStore send pure JSON to the server inside of a single post parameter, but I couldn’t get that to work. The documentation states that this behavior is the default, but it didn’t work that way for me.

4. Pay particular note to

thisGrid.setStore(thisStore, query, {ignoreCase:false});

If you don’t call setStore on the grid this way, it will not send your paramters back to the server correctly the first time. It took me about 10 hours to figure this out. I read documentation, looked at the API and then went digging and I finally found an example where someone is doing it.

All in all, this is a great feature to have on your application, it saves a lot of trouble with paging. One thing that I do as well, it that I wrote a cache on the server side to handle all my data. The first call loads everything in the cache and as you scroll, you are getting data from the cache instead of some backend services. It really comes in handy if you are hitting a mainframe onthe back side as we are.