01
May
11

ExtJS RESTful State Provider


This page describes the process of extending the ExtJS framework to store user preferences in a database instead of browser cookies. For those that want instant gratification, a complete working version of the project is available at the following SVN location. http://ext-jsf.googlecode.com/svn/trunk/wordpress/2011/04/extjs-restful-provider/. Get the project and skip to the Test the Application section of this page.

Background

ExtJS allows components to be customized by the end user. The Extjs grid panel is a good example. The default behaviour of the grid panel allows the user to show or hide columns and change column sorting. However when the user refreshes the page the default settings get restored. In order to have these settings persisted often times developers use the Ext.state.CookieProvider class. This class uses Browser Cookies to store the preference settings. If you have many of these components on your page you may end up storing allot of data on the client browser. Some users also have security enabled software that clean cookie’s often causing these preferences to be lost.

A more effective method is to store these settings on the server side. This page describes how to create a simple database table and RESTful Servlet to allow the extJS component to store this information on the server side.

Requirements

Maven 2

Create a new project

Create a new Project using a Maven archetype.

mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp

groupId: com.test
artifactId: extjs-database-provider

Answer the rest of the questions with defaults “Just hit the enter key”,

cd to the project base folder.

cd extjs-database-provider

Since this is a web project maven2 does not create the java source folder.

Create missing folders now.
mkdir -p src/main/java/com/test
mkdir -p src/main/resources/com/test
mkdir -p src/main/webapp/WEB-INF/jsp

Modify the project Configuration

The pom.xml file should look something like this.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>extjs-database-provider</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>extjs-database-provider Maven Webapp</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>20090211</version>
        </dependency>
        <dependency>
            <groupId>org.apache.ibatis</groupId>
            <artifactId>ibatis-sqlmap</artifactId>
            <version>2.3.4.726</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring</artifactId>
            <version>2.5.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>2.5.6</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.14</version>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>1.8.0.10</version>
        </dependency>
        <dependency>
            <groupId>org.apache.ddlutils</groupId>
            <artifactId>ddlutils</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.0</version>
            <scope>provided</scope>
        </dependency>

  </dependencies>
  <build>
    <finalName>extjs-database-provider</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
				<version>7.0.0.v20091005</version>
                <configuration>
                    <scanIntervalSeconds>2</scanIntervalSeconds>
                    <webAppConfig>
                        <contextPath>/dbProvider</contextPath>
                    </webAppConfig>
                    <stopKey>s</stopKey>
                </configuration>
            </plugin>
        </plugins>
  </build>
</project>

RESTful Provider

The following snippit of code is central to the topic of this blog. It extends the Ext.state.Provider class and implements additional functionality that allows changes to preferences to be sent to the server side.

src/main/webapp/RESTfulProvider.js

/**
 * Provider implementation that stores preferences using a 
 * RESTful Resource.
 */
Ext.state.RESTfulProvider = Ext.extend(Ext.state.Provider, {

    constructor : function(config){ 
        Ext.state.RESTfulProvider.superclass.constructor.call(this);
        this.url = '/';        
        Ext.apply(this, config);
        this.state = this.readPreferenceDB(this);
    },
    set : function(name, value){
    	if(typeof value == "undefined" || value === null) {
    		this.clear(name);
    		return;
    	}
    	this.setPreference(name, value);
	    Ext.state.RESTfulProvider.superclass.set.call(this, name, value);	       		
    },
    clear : function(name){
    	this.clearPreference(name);
        Ext.state.RESTfulProvider.superclass.clear.call(this, name);
    },
    readPreferenceDB : function(provider){
		var xmlhttp = {};
		if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari
		  xmlhttp=new XMLHttpRequest();
		} else { // code for IE6, IE5
			xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
		}
		xmlhttp.open("GET",this.url,false); // make a synchronous request
		xmlhttp.send(null);
		
		var prefs = {};
		if(xmlhttp.status == 200) {
			console.log("response Text: " + xmlhttp.responseText);
			var responseObjArr = Ext.util.JSON.decode(xmlhttp.responseText);			

			// do null checks.
	    	if(typeof responseObjArr == "undefined" || responseObjArr === null) {
	    		return prefs;	    		
	    	}
			var size = responseObjArr.length;
			
			for(var i=0; i<size;i++) {
				var responseObj = responseObjArr[i]; 
				for(key in responseObj) {				
					var value = responseObj[key];
					console.log("key: " + key + ", value: " + value);
					prefs[key] = this.decodeValue(value);
				}				
			}
		} else {
			// there was an error
		}


		return prefs;
    },
    setPreference : function(name, value) {
    	var restfulProvider = this;
	    Ext.Ajax.request({ // makes an async request
	       url: this.url,
	       method: 'POST',
	       params: 'name=' + escape(name) + '&value=' + escape(this.encodeValue(value)),
	       success: function(response, options) { // call back for the async request
	       		console.log('value asynchronously set in database' + response.responseText);
	       },
	       failure: function(response, options) { 
	       		//console.log('defaulting to cookies.');
	       }      
	    });    	
    },
    clearPreference : function(name){
	    Ext.Ajax.request({ // makes an async request
		       url: this.url + "/" + escape(name),
		       method: 'DELETE',
		       success: function(response, options) { // call back for the async request
		       		console.log('value asynchronously deleted in database' + response.responseText);
		       },
		       failure: function(response, options) { 
		       		//console.log('defaulting to cookies.');
		       }      
		    });    	    	
    }
});

Database Setup and Configuration

The following configuration file is used by Apache DDLUtils to generate apply the schema changes each time the application restarts. Changes are made incrementally and every attempt is made to preserve the data. For more information about DDLUtils please see my other tutorial.

src/main/resources/ddl.xml

<?xml version="1.0"?>
<!DOCTYPE database SYSTEM "http://db.apache.org/torque/dtd/database.dtd">
 
<database name="preferencedb">
	<table name="ExtState">
		<column name="domain" type="VARCHAR" size="50" required="true"	primaryKey="true" />
		<column name="userName" type="VARCHAR" size="50" required="true" primaryKey="true" />
		<column name="name" type="VARCHAR" size="20" required="true" primaryKey="true"/>
		<!--Store timestamp as well???-->
		<column name="timestamp" type="TIMESTAMP" />
		<column name="value" type="VARCHAR" size="255" />
		<index name="ExtState_x">
			<index-column name="domain" />
			<index-column name="userName" />
		</index>
	</table>
</database>

Since we are working with HSQLDB, it is critical that we gracefully shut-down the database when the application stops. Otherwise the data that was cached in memory will be lost.

We will use a “ServletContextListener” to listen for the application shut-down event. This will allow us to execute code to graceful shut-down the HSQLDB database.
src/main/java/com/test/DBLifecycleContextListener.java

package com.test;
 
import java.io.InputStreamReader;
 
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.sql.DataSource;
 
import org.apache.ddlutils.Platform;
import org.apache.ddlutils.PlatformFactory;
import org.apache.ddlutils.io.DatabaseIO;
import org.apache.ddlutils.model.Database;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
 
/**
 * This listener initializes or gracefully shuts down the database based on
 * events from the web application.
 */
public class DBLifecycleContextListener implements ServletContextListener {
    private DataSource dataSource = null;
    private WebApplicationContext springContext = null;
 
    public void contextDestroyed(ServletContextEvent event) {
        SimpleJdbcTemplate template = new SimpleJdbcTemplate(dataSource);
        template.update("SHUTDOWN;");
        System.out.println("Database Successfully Shutdown.");
    }
 
    public void contextInitialized(ServletContextEvent event) {
        springContext = WebApplicationContextUtils
                .getWebApplicationContext(event.getServletContext());
        dataSource = (DataSource) springContext.getBean("dataSource");
 
        Platform platform = PlatformFactory
                .createNewPlatformInstance(dataSource);
 
        Database database = new DatabaseIO().read(new InputStreamReader(
                getClass().getResourceAsStream("/ddl.xml")));
 
        platform.alterTables(database, false);
    }
}

Spring Application

The Spring application consists of a

  • ExtState – POJO that represents an individual state item
  • ExtStateModel – Service Interface
  • ExtStateModelImpl – Implementation of the Service
  • ExtStateDataManager – DataManager Interface
  • ExtStateDataManagerImpl – DataManager Implementation

The majority of the configuration is Annotation Based. For more information on annotation based spring configuration please see my other tutorial “using annotations in the spring framework“. Also checkout the following: “hello world spring MVC with annotations“.

ExtState

src/main/java/com/test/ExtState.java

package com.test;

import java.util.Date;

/**
 * This class represents an individual state of a ExtJS component.
 */
public class ExtState {
	private String domain;
	private String userName;
	private String name;
	private String value;
	private Date timestamp = new Date(); // default current timestamp
	
	public String getDomain() {
		return domain;
	}
	public void setDomain(String domain) {
		this.domain = domain;
	}
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getValue() {
		return value;
	}
	public void setValue(String value) {
		this.value = value;
	}
	public Date getTimestamp() {
		return timestamp;
	}
	public void setTimestamp(Date timestamp) {
		this.timestamp = timestamp;
	}	
}

Spring Model classes

The following interface and implementation classes are currently being used a pass-thru to the data manager. Its not adding any value to the application but once we start using Spring Transactions we will be glad we did it this way.

src/main/java/com/test/ExtStateModel.java

package com.test;

import java.util.List;

/**
 * Manages the CRUD operations for the ExtJS State for a given domain user.
 */
public interface ExtStateModel {
	    public ExtState getOne(String domain, String userName, String name);
	 
	    public List<ExtState> get(String domain, String userName);

	    public ExtState post(ExtState extState);
	 
	    public void put(ExtState extState);
	 
		public void delete(ExtState extState);	    	
}

src/main/java/com/test/ExtStateModelImpl.java

package com.test;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.stereotype.Service;

@Service
public class ExtStateModelImpl implements ExtStateModel {

	private ExtStateDataManager extStateDataManager;

	
	private void validateInput(ExtState extState) {
		if (extState.getDomain() == null
				|| "".equals(extState.getDomain().trim())) {
			throw new IllegalArgumentException("Domain is required.");
		}
		if (extState.getUserName() == null
				|| "".equals(extState.getUserName().trim())) {
			throw new IllegalArgumentException("Username is required.");
		}
		if (extState.getName() == null || "".equals(extState.getName().trim())) {
			throw new IllegalArgumentException("name is required.");
		}		
	}
	
	@Override
	public ExtState post(ExtState extState) {
		validateInput(extState);
		ExtState extStateResult = extStateDataManager.read(extState.getDomain(), extState.getUserName(),
				extState.getName());
		
		if(extStateResult==null) { // create one
			extState = extStateDataManager.create(extState);			
		} else {
			extStateDataManager.update(extState);			
		}

		return extState;
	}

	@Override
	public void delete(ExtState extState) {
		validateInput(extState);
		extStateDataManager.delete(extState);
	}

	@Override
	public List<ExtState> get(String domain, String userName) {
		return extStateDataManager.readAll(domain, userName);
	}

	@Required
	@Autowired
	public void setExtStateDataManager(ExtStateDataManager extStateDataManager) {
		this.extStateDataManager = extStateDataManager;
	}

	@Override
	public ExtState getOne(String domain, String userName, String name) {
		return extStateDataManager.read(domain, userName, name);
	}

	@Override
	public void put(ExtState extState) {
		post(extState);
	}

}

src/main/java/com/test/ExtStateDataManager.java

package com.test;

import java.util.List;

public interface ExtStateDataManager {

	public ExtState create(ExtState item);

	public ExtState read(String domain, String userName, String name);

	public List<ExtState> readAll(String domain, String userName);

	public void update(ExtState extState);

	public void delete(ExtState extState);
}

src/main/java/com/test/ExtStateDataManagerImpl.java

package com.test;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.orm.ibatis.SqlMapClientTemplate;
import org.springframework.stereotype.Repository;

@Repository("extStateDataManager")
public class ExtStateDataManagerImpl implements ExtStateDataManager {

    private SqlMapClientTemplate sqlMapClientTemplate;

    public SqlMapClientTemplate getSqlMapClientTemplate() {
        return sqlMapClientTemplate;
    }
 
    @Autowired
    @Required
    public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) {
        this.sqlMapClientTemplate = sqlMapClientTemplate;
    }
    
	@Override
	public ExtState create(ExtState extState) {
        getSqlMapClientTemplate().insert("extStateInsert", extState);
        return extState;
	}

	@Override
	public void delete(ExtState extState) {
        if(extState != null) {
            getSqlMapClientTemplate().delete("extStateDelete", extState);
        }

	}

	@Override
	public ExtState read(String domain, String userName, String name) {
		ExtState extState = new ExtState();
		extState.setDomain(domain);
		extState.setUserName(userName);
		extState.setName(name);
		
        return (ExtState)getSqlMapClientTemplate().queryForObject("extStateInfo", extState);
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<ExtState> readAll(String domain, String userName) {
        List<ExtState> list = null;
        
		ExtState extState = new ExtState();
		extState.setDomain(domain);
		extState.setUserName(userName);
		
        list = getSqlMapClientTemplate().queryForList("allExtState", extState, 0, 1000); 
        return list;
	}

	@Override
	public void update(ExtState extState) {
        getSqlMapClientTemplate().update("extStateUpdate", extState);

	}	

}

Spring Configuration

Most of the spring configuration was done using Java Annotations, therefore this file is not too large.

src/main/resources/applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
 
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
 
http://www.springframework.org/schema/context
 
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
 
	<context:annotation-config/>
    <context:component-scan base-package="com.test"/>

	<bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:file:src/main/webapp/WEB-INF/db/testdb"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
	</bean>
	
<!-- Used for Ibatis -->
    <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="configLocation" value="/WEB-INF/mapper-config.xml"/>
        <property name="dataSource" ref="dataSource"/>
        <property name="useTransactionAwareDataSource" value="true"/>
    </bean>
    <bean id="sqlMapClientTemplate" class="org.springframework.orm.ibatis.SqlMapClientTemplate">
        <property name="sqlMapClient" ref="sqlMapClient"/>
    </bean>
    	
</beans>

Integration with IBATIS

The following needs to be defined in the spring configuration. The SQLMapClientTemplate is the primary class that will be used by our data-mangers to query the database thru SQL statements and stored procedure calls. It needs a reference to sqlMapClient that we just created.

The following is the ibatis main configuration file. Since we are using spring framework there is not much configuration that you need to put in here. All that is required is that ibatis knows where the xml files for all the Data Managers are located.

For more information on ibatis please see my other tutorial “ibatis 2.3 spring 2.5 hello world

src/main/webapp/WEB-INF/mapper-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
        PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
        "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
    <sqlMap resource="com/test/extStateDataManager.xml"/>
</sqlMapConfig>

src/main/resources/com/test/extStateDataManager.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
        PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
        "http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="ExtState">
 
<typeAlias alias="ExtState" type="com.test.ExtState" />
 
    <select id="allExtState" resultClass="ExtState" parameterClass="ExtState">
        SELECT domain as domain,
        	userName as userName,
        	name as name,
        	value as value,
        	timestamp as timestamp
        FROM ExtState where domain = #domain# and userName = #userName#;
    </select>
    <select id="extStateInfo" parameterClass="ExtState" resultClass="ExtState">
        SELECT domain as domain,
        	userName as userName,
        	name as name,
        	value as value,
        	timestamp as timestamp
        FROM ExtState
        WHERE domain = #domain# and userName = #userName# and name = #name#;
    </select>
  
    <insert id="extStateInsert" parameterClass="ExtState">
        INSERT INTO ExtState (domain, userName, name, value, timestamp)
        VALUES ( #domain#, #userName#, #name#, #value#, #timestamp#);
    </insert>
    <update id="extStateUpdate" parameterClass="ExtState">
        UPDATE ExtState SET domain = #domain#, userName = #userName#, 
        	name = #name#, value = #value#, timestamp = #timestamp#
        WHERE domain = #domain# and userName = #userName# and name = #name#;
    </update>
    <delete id="extStateDelete" parameterClass="ExtState">
        DELETE FROM ExtState 
        WHERE domain = #domain# and userName = #userName# and name = #name#;
    </delete>
</sqlMap>

Servlet

The following servlet takes requests to save preferences from the Restful State Provider and calls the data-managers to save it to the database.

src/main/java/com/test/DBPrefServlet.java

package com.test;

import java.io.IOException;
import java.io.PrintWriter;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

public class DBPrefServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	private ExtStateModel extStateModel;

	// domain is application wide.
	private static final String domain = "com.test";

	
	@Override
	public void init(ServletConfig config) throws ServletException {
		ApplicationContext context = WebApplicationContextUtils
				.getWebApplicationContext(config.getServletContext());
		extStateModel = (ExtStateModel) context.getBean("extStateModelImpl");
		super.init(config);
	}
	
	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {

		Principal userBean = (Principal)req.getSession().getAttribute("userBean");
		if (userBean == null || userBean.getName() == null
				|| "".equals(userBean.getName().trim())) {
			// not authorized return HTTP 401      
			resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
			return;
		}

		String userName = userBean.getName();

		List<ExtState> extStateList = new ArrayList<ExtState>(0);

		final String pattern = "/*";  
		String[] names = { "name" };
		Map<String, String> pathMap = PathUtil.getPathVariables(pattern, names,
		req.getPathInfo());
		String name = pathMap.get("name");
		 
		if(name == null || "".equals(name.trim())) {
			// get all results
			extStateList = extStateModel.get(domain, userName);    	  
		} else {
			// get results for provided name 
			ExtState extState = extStateModel.getOne(domain, userName, name);
			if(extState != null) {
				extStateList = Collections.singletonList(extState);
			}
		}
		 
		if(extStateList == null || extStateList.size() == 0) {
			// nothing found return a HTTP 404
			resp.sendError(HttpServletResponse.SC_NOT_FOUND,
				"Ext preference not found for the currently logged in user.");
		} 
		else { // this is for debugging and other api usage.
			JSONArray jsonArray = new JSONArray();
			for(ExtState extState : extStateList) {
				JSONObject jsonObject = new JSONObject();
				try {
					jsonObject.put(extState.getName(), extState.getValue());
				} catch (JSONException e) {
					throw new RuntimeException(e);
				}
				jsonArray.put(jsonObject);
			}
			PrintWriter writer = resp.getWriter();
			writer.write(jsonArray.toString());
		}
	}
	
	@Override
	public void doPut(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {

		
		Principal userBean = (Principal)req.getSession().getAttribute("userBean");
		if (userBean == null || userBean.getName() == null
				|| "".equals(userBean.getName().trim())) {
			resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
			return;
		}

		String userName = userBean.getName();
		
		String name = req.getParameter("name");
		String value = req.getParameter("value");
		
		//TODO: validate name and value!		
		ExtState extState = new ExtState();
		
		extState.setDomain(domain);
		extState.setUserName(userName);
		extState.setName(name);
		extState.setValue(value);
		
		extStateModel.put(extState);
		
		PrintWriter writer = resp.getWriter();
		writer.write(new JSONObject(extState).toString());			

	}
	
	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		doPut(req, resp);
	}
	
	@Override
	public void doDelete(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {

		Principal userBean = (Principal)req.getSession().getAttribute("userBean");
		if (userBean == null || userBean.getName() == null
				|| "".equals(userBean.getName().trim())) {
			resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
			return;
		}

		String userName = userBean.getName();

		final String pattern = "/*";  
		String[] names = { "name" };
		
		Map<String, String> pathMap = PathUtil.getPathVariables(pattern, names,
		req.getPathInfo());
		String name = pathMap.get("name");

		if(name==null || "".equals(name.trim())) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "name is required");
			return;
		}   
		ExtState extState = new ExtState();
		extState.setDomain(domain);
		extState.setUserName(userName);
		extState.setName(name);
		
		extStateModel.delete(extState);				
	}
}

Login Servlet

This servlet is used to log the user into the system and create a HttpSession.

src/main/java/com/test/LoginServlet.java

package com.test;

import java.io.IOException;
import java.security.Principal;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		
		final String pattern = "/*";  
		String[] names = { "userName" };
		Map<String, String> pathMap = PathUtil.getPathVariables(pattern, names,
		req.getPathInfo());
		String userName = pathMap.get("userName");

		if(userName==null || "".equals(userName.trim())) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "");
			return;
		}
		Principal userBean = (Principal)req.getSession().getAttribute("userBean");
		if(userBean == null || !userName.equals(userBean.getName())) {
			resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
			return;			
		}		
	}
	
	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		
		String userName = req.getParameter("userName");
		String password = req.getParameter("password");		
		// TODO: validate input...
		
		if(!"tiger".equals(password)) {
			resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
			return;
		} 
		
		// for testing purposes allow any username 
		// to be set if password is correct.
		req.getSession().setAttribute("userBean", new UserBean(userName));
		System.out.println("set the userbean in session");
	}
	
	@Override
	public void doDelete(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		req.getSession().removeAttribute("userBean");
	}
}

src/main/java/com/test/UserBean.java

package com.test;

import java.security.Principal;

public class UserBean implements Principal {

	private String name;
	
	public UserBean() {
		super();
	}
	public UserBean(String name) {
		this.setName(name);
	}
	
	@Override
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
}

PathUtil

For more information on this Util class click here.

src/main/java/com/test/PathUtil.java

package com.test;
 
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * Helper class to allow URL path's to be parsed into
 * EL Expression type tokens.
 */
public class PathUtil {
    public static final Map<String, String> getPathVariables(String patternStr,
            String[] names, String path) {
        String regExPattern = patternStr.replace("*", "(.*)");
 
        Map<String, String> tokenMap = new HashMap<String, String>();
 
        if(path==null || "".equals(path.trim())) {
        	return tokenMap;
        }
        
        Pattern p = Pattern.compile(regExPattern);
 
        Matcher matcher = p.matcher(path);
 
        if (matcher.find()) {
            // Get all groups for this match
            for (int i = 0; i <= matcher.groupCount(); i++) {
                String groupStr = matcher.group(i);
                if (i != 0) {
                    tokenMap.put(names[i - 1], groupStr);
                }
            }
        }
        return tokenMap;
    }
}

Main Page

The following is the main page of the application. It sets up 2 grid components with 2 different stores. It hooks those components up to the Restful provider.

src/main/webapp/index.jsp

<html>
<head>

<link rel="stylesheet" type="text/css"
	href="http://dev.sencha.com/deploy/dev/resources/css/ext-all.css" />

<script type="text/javascript"
	src="http://dev.sencha.com/deploy/dev/adapter/ext/ext-base-debug.js">
</script>
<script type="text/javascript"
	src="http://dev.sencha.com/deploy/dev/ext-all-debug.js">
</script>


<script type="text/javascript">
Ext.Ajax.on('requestcomplete', function(conn, response, options) {
	var responsediv = Ext.get('responsediv');
	responsediv.dom.innerHTML = response.responseText;
}, this);
Ext.Ajax.on('requestexception', function(conn, response, options) {
	var responsediv = Ext.get('responsediv');
	responsediv.dom.innerHTML = response.responseText;
}, this);

var submitForm = function(formid, resource, method) {
	Ext.Ajax.request({
	   url: resource,
	   method: method,
	   form: formid,
	   params: ''
	});	
};
</script>
<script type="text/javascript" src='RESTfulProvider.js'></script>
     
<script type="text/javascript">
function buildWindow() { 
	var arrayData = [
		['Jay Garcia',    'MD'],
		['Aaron Baker',   'VA'],
		['Susan Smith',   'DC'],
		['Mary Stein',    'DE'],
		['Bryan Shanley', 'NJ'],
		['Nyri Selgado',  'CA']
	];
	 
	var store = new Ext.data.ArrayStore({
	  data   : arrayData,
	  fields : ['fullName', 'state']
	});
	
	var store2 = new Ext.data.ArrayStore({
	  data   : arrayData,
	  fields : ['fullName', 'state']
	});
	 
	var cm = new Ext.grid.ColumnModel([{
        header    : 'Full Name',
        sortable  : true,
        dataIndex : 'fullName'
    },
    {
	    header    : 'State',
		renderer : function(value, cell) {
	    	return '<a href="#">' + value + '</a>'
		},
        dataIndex : 'state'
    }
]);
	var cm2 = new Ext.grid.ColumnModel([{
        header    : 'Full Name',
        sortable  : true,
        dataIndex : 'fullName'
    },
    {
	    header    : 'State',
		renderer : function(value, cell) {
	    	return '<a href="#">' + value + '</a>'
		},
        dataIndex : 'state'
    }
]);
	
	var dbProvider = new Ext.state.RESTfulProvider({url : 'extState/'});
	
	Ext.state.Manager.setProvider(dbProvider); 
	
	var grid = new Ext.grid.GridPanel({
		title		: 'Stateful Grid',
		renderTo	: 'grid-panel',
		autoHeight	: true,
		width		: 250,
		stateful	: true,
		stateId		: 'my_grid_panel',                       
		store		: store,
		colModel	: cm2
	});
	
	var grid2 = new Ext.grid.GridPanel({
		title		: 'Stateful Grid2',
		renderTo	: 'grid-panel2',
		autoHeight	: true,
		width		: 250,
		stateful	: true,
		stateId		: 'my_grid_panel2',                       
		store		: store2,
		colModel	: cm
	});
}
 
Ext.onReady(buildWindow);    
</script>
</head>
<body>

<h3>RESTful State Provider Example</h3>
<br></br>
This page stores grid preferences in a HyperSQL database.
<br/><br/>
<strong>Instructions:</strong>
<ol>
<li>1. Login by entering a username.</li>
<li>2. Click on the first Name. Or remove a column.</li>
<li>3. Refresh the page. Preferences should maintain</li>
<li>4. Logout and login as another user.</li>
<li>5. Repeat steps 2, 3</li>
<li>6. Login as the first user.</li>
<li>7. Preferences should have remained the same.</li>
<li>8. click GET in the extState Manager form to inspect database values</li>
<li>9. Use FireBug to see Ajax Communication</li>
<li>10. Shutdown the server and view the database /WEB-INF/db/testdb.script</li>
</ol>

<br/>

<div style="float:left;padding-right:50px;" id="grid-panel"></div>
<div style="float:left;padding-right:50px;" id="grid-panel2"></div>

<h2>Login Form</h2>

<form id='form1'>
<label>userName</label><input name="userName"
	type="text" /><br />
<label>password</label><input name="password"
	type="password" value="tiger"/><br />

<input value="Check Status" type="button" onclick="submitForm('', 'login/' + escape(this.form.userName.value), 'GET');" />
<input value="Login" type="button" onclick="submitForm('form1', 'login', 'POST');" /> 
<input value="Logout" type="button" onclick="submitForm('form1', 'login/', 'DELETE');" />
</form>

<h3>extState Manager</h3>
<form id='form2'><label>name</label><input name="name"
	type="text" /><br />
<label>value</label><input name="value" type="text" /><br />
<input value="GET" type="button"
	onclick="submitForm('', 'extState/' + escape(this.form.name.value), 'GET');" />
<input value="POST" type="button"
	onclick="submitForm('form2', 'extState', 'POST');" /> 
<input
	value="DELETE" type="button"
	onclick="submitForm('form2', 'extState/' + escape(this.form.name.value), 'DELETE');" />
</form>

<h3>Response:</h3>
<div id="responsediv"></div>

</body>
</html>

Web Application Configuration

src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    id="WebApp_ID" version="2.5">
    
	<display-name>Archetype Created Web Application</display-name>

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:applicationContext.xml</param-value>
	</context-param>

	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<listener>
		<listener-class>com.test.DBLifecycleContextListener</listener-class>
	</listener>

    <servlet>
        <servlet-name>dbPrefServlet</servlet-name>
        <servlet-class>com.test.DBPrefServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>loginServlet</servlet-name>
        <servlet-class>com.test.LoginServlet</servlet-class>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>loginServlet</servlet-name>
        <url-pattern>/login/*</url-pattern>
    </servlet-mapping>
 
    <servlet-mapping>
        <servlet-name>dbPrefServlet</servlet-name>
        <url-pattern>/extState/*</url-pattern>
    </servlet-mapping>

</web-app>

Test the application

Start Jetty Servlet Engine

mvn clean compile jetty:run

Test the application

Navigate to: http://localhost:8080/dbProvider/ and enter some data

The page will not store preferences unless you login. Enter an ID and click login. You can also switch to another user by logging out and logging in as another user. No need to provide password since it is already pre-filled. Switching between users will allow you to test the ability to customize the components per-user basis.

Test the following as well.

  1. then shutting down jetty – CTRL-C
  2. verify the data survived in the src/main/webapp/WEB-INF/db directory.

Creating a WAR file

If you want to run the application in another environment you can create a war file and drop it into a server of your choice. This method will also package up any runtime dependencies and put them in the /WEB-INF/lib folder of the WAR file.

mvn clean compile package

The war file should appear in the “target/” folder.

Advertisements

2 Responses to “ExtJS RESTful State Provider”


  1. January 10, 2012 at 10:31 am

    Thanks for this post, i will use it in my work (i need server side state provider for extjs in Java).


Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 74 other followers

May 2011
S M T W T F S
« Apr   Jul »
1234567
891011121314
15161718192021
22232425262728
293031  

Blog Stats

  • 801,397 hits

%d bloggers like this: