Posts Tagged ‘RESTful

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
23
Apr
11

Nested JSON

This page describes the process of performing CRUD operations on Java Beans that have Nested Properties using ExtJS.

Requirements

The Item bean contains a length, width, and height property. We will move those attributes to a new bean called Dimension and add a dimension attribute to the Item class.

This will result in the editor Grid submitting the following xml in the Request Body:

<?xml version="1.0" encoding="UTF-8"?>
	<item>
		<itemId>5</itemId>
		<name>tes</name>
		<description>111111</description>
		<stockQty>2</stockQty>
		<color>blue</color>
		<dimension.length>1</dimension.length>
		<dimension.width>1</dimension.width>
		<dimension.height>1</dimension.height>
	</item>

The following changes need to be done:

  1. Create a new Dimension class, modify the Item class to use it.
  2. Modify the IBATIS sql map xml files
  3. Modify ExtJS JsonReader to read the nested information

src/main/java/com/test/Dimension.java

package com.test;

import java.math.BigDecimal;

public class Dimension {
    private BigDecimal length;
    private BigDecimal width;
    private BigDecimal height;
    
    
	public BigDecimal getLength() {
		return length;
	}
	public void setLength(BigDecimal length) {
		this.length = length;
	}
	public BigDecimal getWidth() {
		return width;
	}
	public void setWidth(BigDecimal width) {
		this.width = width;
	}
	public BigDecimal getHeight() {
		return height;
	}
	public void setHeight(BigDecimal height) {
		this.height = height;
	}
}

Item

Replace the implementation of Item.java with the following.

src/main/java/com/test/Item.java

package com.test;
 
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlRootElement;
 
/**
 * Represents an item in inventory or in a shopping cart.
 */
@XmlRootElement
public class Item {
    private Integer itemId;
    private String oemId;
    private String name;
    private String description;
    private String imageURL;
    private String color;
    private BigDecimal price;
    private Dimension dimension = new Dimension();
     
    /**
     * Weight in grams
     */
    private BigDecimal weight;
    /**
     * Quantity in stock.
     */
    private Integer stockQty;
 
    public Item() {
        super();
    }
 
    public Item(Integer id, String name) {
        this.name = name;
        this.itemId = id;
    }
 
    public Integer getItemId() {
        return itemId;
    }
    public void setItemId(Integer itemId) {
        this.itemId = itemId;
    }
    public String getOemId() {
        return oemId;
    }
    public void setOemId(String oemId) {
        this.oemId = oemId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public String getImageURL() {
        return imageURL;
    }
    public void setImageURL(String imageURL) {
        this.imageURL = imageURL;
    }
    public BigDecimal getPrice() {
        return price;
    }
    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public BigDecimal getWeight() {
        return weight;
    }
    public void setWeight(BigDecimal weight) {
        this.weight = weight;
    }
 
    public Integer getStockQty() {
        return stockQty;
    }
 
    public void setStockQty(Integer stockQty) {
        this.stockQty = stockQty;
    }

    public void setDimension(Dimension dimension) {
		this.dimension = dimension;
    }

    public Dimension getDimension() {
		return dimension;
    }
}

IBATIS SQL Mapper

Change the SQL mapper configuration file to look like this…

src/main/resources/com/test/ItemDataManager.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="Item">
 
<typeAlias alias="Item" type="com.test.Item" />

    <resultMap id="itemResultMap" class="Item">
		<result property="itemId" column="itemId"/>
		<result property="oemId" column="oemId"/>
		<result property="name" column="name"/>
		<result property="description" column="description"/>
		<result property="color" column="color"/>
		<result property="imageURL" column="imageURL"/>
		<result property="price" column="price"/>
		<result property="dimension.length" column="length"/>
		<result property="dimension.width" column="width"/>
		<result property="dimension.height" column="height"/>
		<result property="weight" column="weight"/>
		<result property="stockQty" column="stockQty"/>
    </resultMap>

 
    <select id="allItems" resultMap="itemResultMap" >
        select item_id as itemId,
        oem_id as oemId,
        item_x as name,
        description as description,
        color as color,
        image_url as imageURL,
        price as price,
        length as length,
        width as width,
        height as height,
        weight as weight,
        stock_qty as stockQty
        from item;
    </select>
    <select id="getItemInfo" parameterClass="int" resultMap="itemResultMap" >
        select item_id as itemId,
        oem_id as oemId,
        item_x as name,
        description as description,
        color as color,
        image_url as imageURL,
        price as price,
        length as length,
        width as width,
        height as height,
        weight as weight,
        stock_qty as stockQty
        from item where item_id = #id#;
    </select>
 
    <insert id="itemInsert" parameterClass="Item">
        insert into item (oem_id,
            item_x,
            description,
            color,
            image_url,
            price,
            length,
            width,
            height,
            weight,
            stock_qty)
        values ( #oemId#, #name#, #description#, #color#,
            #imageURL#, #price#, #dimension.length#, #dimension.width#,
            #dimension.height#, #weight#, #stockQty# );
        <selectKey resultClass="int" >
            <!-- HSQLDB -->
            CALL IDENTITY();
            <!-- MySQL -->
            <!-- SELECT LAST_INSERT_ID(); -->
        </selectKey>
    </insert>
    <update id="updateItem" parameterClass="Item">
        update item set oem_id = #oemId#, item_x = #name#, description=#description#,
            color = #color#, image_url = #imageURL#, price = #price#, length = #dimension.length#,
            width = #dimension.width#, height = #dimension.height#, weight = #weight#, stock_qty = #stockQty#
        where item_id = #itemId#;
    </update>
    <delete id="deleteItem" parameterClass="java.lang.Integer">
        delete from item where item_id = #itemId#;
    </delete>
</sqlMap>

Change the Grid

MyApp={};
MyApp.getContext = function() {
	var base = document.getElementsByTagName('base')[0];
	if (base && base.href && (base.href.length > 0)) {
		base = base.href;
	} else {
		base = document.URL;
	}
	return base.substr(0, base.indexOf("/", base.indexOf("/", base.indexOf("//") + 2) + 1));
};

var App = new Ext.App({});
 
var proxy = new Ext.data.HttpProxy({
	url: MyApp.getContext() + '/appdata/itemresource'
});

var reader = new Ext.data.JsonReader({
    totalProperty: 'total',
    successProperty: 'success',
    idProperty: 'itemId',
    root: 'data',
    record: 'item',
    messageProperty: 'message',  // <-- New "messageProperty" meta-data
    fields : [
              {name: 'itemId'},
              {name: 'name', allowBlank: false},
              {name: 'description', allowBlank: true},
              {name: 'stockQty', allowBlank: true},
              {name: 'color', allowBlank: true},
// The 'mapping' field allows binding to complex nested JSON objects              
              {name: 'length', allowBlank: true, mapping: 'dimension.length'},              
              {name: 'width', allowBlank: true, mapping: 'dimension.width'},              
              {name: 'height', allowBlank: true, mapping: 'dimension.height'}              
    ]
});

// The new DataWriter component.
var writer = new Ext.data.XmlWriter({
    encode : false,
    writeAllFields : true,
    xmlEncoding : "UTF-8"
});

// Typical Store collecting the Proxy, Reader and Writer together.
var store = new Ext.data.Store({
    id: 'item',
    restful: true,     // <-- This Store is RESTful
    proxy: proxy,
    reader: reader,
    writer: writer    // <-- plug a DataWriter into the store
});

// load the store
store.load();

// The following is necessary to map the Record Fields to the grid columns.
var userColumns =  [
    {header: "Item Id", width: 40, sortable: true, dataIndex: 'itemId'},
    {header: "Item Name", width: 100, sortable: true, dataIndex: 'name', editor: new Ext.form.TextField({})},
    {header: "Description", width: 100, sortable: true, dataIndex: 'description', editor: new Ext.form.TextField({})},
    {header: "Quantity", width: 100, sortable: true, dataIndex: 'stockQty', editor: new Ext.form.TextField({})},
    {header: "Color", width: 100, sortable: true, dataIndex: 'color', editor: new Ext.form.TextField({})},
    {header: "Length", width: 100, sortable: true, 
    	dataIndex: 'length', editor: new Ext.form.TextField({})},
    {header: "Width", width: 100, sortable: true, 
    	dataIndex: 'width', editor: new Ext.form.TextField({})},
    {header: "Height", width: 100, sortable: true, 
    	dataIndex: 'height', editor: new Ext.form.TextField({})}
];

Ext.onReady(function() {
    Ext.QuickTips.init();

    // use RowEditor for editing
    var editor = new Ext.ux.grid.RowEditor({
        saveText: 'Save'
    });

    // Create a typical GridPanel with RowEditor plugin
    var userGrid = new Ext.grid.GridPanel({
        renderTo: 'user-grid',
        iconCls: 'icon-grid',
        frame: true,
        title: 'Fun with RESTful CRUD',
        height: 300,
        store: store,
        plugins: [editor],
        columns : userColumns,
    	enableColumnHide: false,
        tbar: [{
        	id: 'addBtn',
            text: 'Add',
            iconCls: 'silk-add',
            handler: onAdd
        }, '-', {
        	id: 'deleteBtn',
            text: 'Delete',
            iconCls: 'silk-delete',
            handler: onDelete
        }, '-'],
        viewConfig: {
            forceFit: true
        }
    });

    function onAdd(btn, ev) {
        var u = new userGrid.store.recordType({
            first : '',
            last: '',
            email : ''
        });
        editor.stopEditing();
        userGrid.store.insert(0, u);
        editor.startEditing(0);
    }

    // Disable the buttons when editing.
    editor.addListener('beforeedit', function(roweditor, rowIndex){
        Ext.getCmp('addBtn').disable();
        Ext.getCmp('deleteBtn').disable();
    });
    editor.addListener('afteredit', function(roweditor, rowIndex){
        Ext.getCmp('addBtn').enable();
        Ext.getCmp('deleteBtn').enable();
    });
    editor.addListener('canceledit', function(roweditor, rowIndex){
    	// get fresh records from the database.
        userGrid.store.reload({});
        Ext.getCmp('addBtn').enable();
        Ext.getCmp('deleteBtn').enable();
    });     

    function onDelete() {
        var rec = userGrid.getSelectionModel().getSelected();
        if (!rec) {
            return false;
        }
        userGrid.store.remove(rec);
    }
    activateGridValidation(userGrid);
});

Test the application

In the project top folder type in the following command to compile and run the code in jetty servlet engine.

mvn clean compile jetty:run

Navigate to the following URL:
http://localhost:8080/crud/

Modify the application presented in Part II

In this section we will modify the application presented in part II of my posting in this series.

Regular form panel can submit nested bean information. The submission looks like this:

name=4&description=444444&stockQty=4&color=blue&dimension.length=4&dimension.width=4&dimension.height=4

Modify the form.html page to look like this…

src/main/webapp/form.html

<html>
<head>
 
<link rel="stylesheet" type="text/css"
    href="http://dev.sencha.com/deploy/ext-3.3.1/resources/css/ext-all.css"/>
 
<script type="text/javascript"
    src="http://dev.sencha.com/deploy/ext-3.3.1/adapter/ext/ext-base-debug.js">
</script>
<script type="text/javascript"
    src="http://dev.sencha.com/deploy/ext-3.3.1/ext-all-debug.js">
</script>
 
<script type="text/javascript">
 
MyApp={};
MyApp.getContext = function() {
    var base = document.getElementsByTagName('base')[0];
    if (base && base.href && (base.href.length > 0)) {
        base = base.href;
    } else {
        base = document.URL;
    }
    return base.substr(0, base.indexOf("/", base.indexOf("/", base.indexOf("//") + 2) + 1));
};
 
function buildWindow() {
    Ext.QuickTips.init();
    Ext.form.Field.prototype.msgTarget = 'side';
    var bd = Ext.getBody();
 
    var fs = new Ext.FormPanel({
        labelWidth: 75,
        frame: true,
        title:'Form with crazy amount of validation',
        bodyStyle:'padding:5px 5px 0',
        defaults: {width: 230},
        waitMsgTarget: true,
        defaultType: 'textfield',    
 
        items: [{
                fieldLabel: 'Item Name',
                name: 'name'
            },{
                fieldLabel: 'Description',
                name: 'description'
            },{
                fieldLabel: 'Quantity',
                name: 'stockQty'
            }, {
                fieldLabel: 'Color',
                name: 'color'
            }, {
                fieldLabel: 'Length',
                name: 'dimension.length'
		    }, {
		        fieldLabel: 'Width',
		        name: 'dimension.width'
			}, {
			    fieldLabel: 'Height',
			    name: 'dimension.height'
			}
        ],    
 
    });
 
    var onSuccessOrFail = function(form, action) {
        var result = action.result;
 
        if(result.success) {
            Ext.MessageBox.alert('Success', 'Success!');
            userGrid.store.reload({});
        } else { // put code here to handle form validation failure.
        }
 
    }
 
    var submit = fs.addButton({
        text: 'Submit',
        disabled:false,
        handler: function(){
            fs.getForm().submit({
                url:MyApp.getContext() +'/app/itemresource',
                waitMsg:'Submitting Data...',
                submitEmptyText: false,
                success : onSuccessOrFail,
                failure : onSuccessOrFail
            });
        }
    });
 
    fs.render('form-first');
 
    // the following is necessary to display the grid.
 
// The following is necessary to map the Record Fields to the grid columns.
var userColumns =  [
    {header: "Item Id", width: 40, sortable: true, dataIndex: 'itemId'},
    {header: "Item Name", width: 100, sortable: true, dataIndex: 'name', editor: new Ext.form.TextField({})},
    {header: "Description", width: 100, sortable: true, dataIndex: 'description', editor: new Ext.form.TextField({})},
    {header: "Quantity", width: 100, sortable: true, dataIndex: 'stockQty', editor: new Ext.form.TextField({})},
    {header: "Color", width: 100, sortable: true, dataIndex: 'color', editor: new Ext.form.TextField({})},
    {header: "Length", width: 100, sortable: true, 
    	dataIndex: 'length', editor: new Ext.form.TextField({})},
    {header: "Width", width: 100, sortable: true, 
    	dataIndex: 'width', editor: new Ext.form.TextField({})},
    {header: "Height", width: 100, sortable: true, 
    	dataIndex: 'height', editor: new Ext.form.TextField({})}
];
 
    var proxy = new Ext.data.HttpProxy({
        url: MyApp.getContext() + '/app/itemresource'
    });
 
    var reader = new Ext.data.JsonReader({
        totalProperty: 'total',
        successProperty: 'success',
        idProperty: 'itemId',
        root: 'data',
        record: 'item',
        messageProperty: 'message',  // <-- New "messageProperty" meta-data
        fields : [
                  {name: 'itemId'},
                  {name: 'name', allowBlank: false},
                  {name: 'description', allowBlank: true},
                  {name: 'stockQty', allowBlank: true},
              {name: 'color', allowBlank: true},
// The 'mapping' field allows binding to complex nested JSON objects              
              {name: 'length', allowBlank: true, mapping: 'dimension.length'},              
              {name: 'width', allowBlank: true, mapping: 'dimension.width'},              
              {name: 'height', allowBlank: true, mapping: 'dimension.height'}              
        ]
    });
 
    // Typical Store collecting the Proxy, Reader and Writer together.
    var store = new Ext.data.Store({
        id: 'item',
        restful: true,     // <-- This Store is RESTful
        proxy: proxy,
        reader: reader
    });
 // load the store
    store.load();
 
    // Create a typical GridPanel with RowEditor plugin
    var userGrid = new Ext.grid.GridPanel({
        renderTo: 'user-grid',
        iconCls: 'icon-grid',
        frame: true,
        title: 'Records in HyperSQL Database',
        height: 300,
        store: store,
        columns : userColumns,
        enableColumnHide: false,
        viewConfig: {
            forceFit: true
        }
    });
 
}
Ext.onReady(buildWindow);
</script>
</head>
<body>
 
<div class="container" style="width:500px">
    <div id="form-first"></div>
    <div id="user-grid"></div>
</div>
 
</body>
</html>
23
Apr
11

CRUD with Extjs

The content of this page has moved/combined with another page: https://numberformat.wordpress.com/2011/04/07/restful-crud-using-extjs/

07
Apr
11

RESTful CRUD using extJS

This page describes how to create an extJS based web application that performs RESTful CRUD operations on resources in a Java JEE web application. 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/restful-crud-using-extjs/. Get the project and skip to the Test The Application section of this page.

Background

Each Entity object (resource) suppose to have a unique URL in a REST based application. This design consideration makes it easy for JavaScript frameworks like extJS to perform CRUD operations on the resource based on user’s actions.

This tutorial builds from an article about ExtJS + Spring MVC validation using commons-validator. While that page only allowed you to create new items by issuing a HTTP POST, this page will complete the CRUD concept allowing the browser to issue GET, PUT, and DELETE methods to manage items in the database.

CRUD is composed of the following parts:

Create(aka. HTTP POST)

The extJS component issues a HTTP POST to the URL listed in the data store. HTTP POST is neither safe or omnipotent. This means that the HTTP POST can change the state of the object and also multiple invocations can be disruptive (it will create an extra object if called twice by mistake). The component is expecting a JSON “success” string and a representation of the object at a minimum. The id of the item is not passed in the URL because it is not known yet.

Read (aka. HTTP GET)

The extJS component is expecting a list of objects to be returned when it issues a GET to the URL. The list of items are displayed on the grid based on the configuration of the reader or metaData. HTTP GET is a “safe” operation. This means that calls to a resource using GET will not change the state of the resource. The component is expecting a JSON “success” string at a minimum.

Update (aka. HTTP PUT)

The extJS component sends the id in the URL path and the representation of the modified object in the body of the PUT. HTTP PUT is not “safe” however it is “omnipotent”. This means that an HTTP PUT can change the state of the object however multiple submissions of this PUT will not will not be disruptive. The component is expecting a JSON “success” and a representation of the object at a minimum.

Destroy (aka. HTTP DELETE)

The extJS component sends the id in the URL path. HTTP DELETE is not safe however it is omnipotent. This means that DELETE operations will change the state of the object on the server side however multiple invocations of this will not be disruptive. (since deleting the same id the second time will not do anything). The component is expecting a JSON “success” string at a minimum.

This page describes all you need to create and configure a self-contained java application that interacts with an in-memory Java database.

Requirements

Procedure

Verify The Existing Project

Open up a console with to the project’s base directory.

Convert XML to POJO

The Grid on this page uses a XmlWriter to submit the Item as XML in the Request Body. In order to convert the XML into a JavaBean we will be using JAXB.

Converting XML to JavaBeans using JAXB was covered on the following page “using jaxb to convert between xml and pojo“.

Modify Item.java and define “@XmlRootElement” right above the class name and add the extra import statement.

src/main/java/com/test/Item.java

...
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Item {
...

The following class is used by JAXB to create an instance of an Item Bean.

src/main/java/com/test/ObjectFactory.java

package com.test;

import javax.xml.bind.annotation.XmlRegistry;

@XmlRegistry
public class ObjectFactory {
 
    public ObjectFactory() {
    }
 
    public Item createItem() {
        return new Item();
    }
}

Binding the Bean from the Request Body

The request body contains XML representation of beans. Spring MVC 2.5 has a @ModelAttribute annotation that does this for us. However the default implementation expects the input as HTTP form attributes. For example (“itemId=1&name=testName&description=TestDescription”). Since the Grid will be posting the Bean as XML we need to override a few classes.

The following 4 classes need to be created:

  1. CustomAnnotationMethodHandlerAdapter.java
  2. XMLServletRequestDataBinder.java
  3. ServletRequestBodyXMLPropertyValues.java
  4. XMLWebUtils.java

src/main/java/com/test/fw/CustomAnnotationMethodHandlerAdapter.java

package com.test.fw;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;

/**
 * The whole purpose of this class it to allow it to specify XMLServletRequestDataBinder
 * instead of the ServletRequestDataBinder as defined in the parent class.
 */
public class CustomAnnotationMethodHandlerAdapter extends AnnotationMethodHandlerAdapter {
	@Override
	protected ServletRequestDataBinder createBinder(HttpServletRequest request,
			Object target, String objectName) throws Exception {		
		return new XMLServletRequestDataBinder(target, objectName);
	}
}

src/main/java/com/test/fw/XMLServletRequestDataBinder.java

package com.test.fw;

import javax.servlet.ServletRequest;

import org.springframework.beans.MutablePropertyValues;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.multipart.MultipartHttpServletRequest;

/**
 * The purpose of this class it call ServletRequestBodyXMLPropertyValues instead
 * of ServletRequestPropertyValues in the bind method.
 */
public class XMLServletRequestDataBinder extends ServletRequestDataBinder {

	public XMLServletRequestDataBinder(Object target, String objectName) {
		super(target, objectName);
	}

	@Override
	public void bind(ServletRequest request) {
		MutablePropertyValues mpvs = new ServletRequestBodyXMLPropertyValues(request);
		if (request instanceof MultipartHttpServletRequest) {
			MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
			bindMultipartFiles(multipartRequest.getFileMap(), mpvs);
		}
		doBind(mpvs);
	}

}

src/main/java/com/test/fw/ServletRequestBodyXMLPropertyValues.java

package com.test.fw;

import javax.servlet.ServletRequest;

import org.springframework.beans.MutablePropertyValues;


/**
 * PropertyValues implementation created from XML in the ServletRequest body. This 
 * implementation uses the XMLWebUtils instead of WebUtils to extract property values
 * from the ServletRequest body.
 */
public class ServletRequestBodyXMLPropertyValues extends MutablePropertyValues {
	private static final long serialVersionUID = 1L;

	public ServletRequestBodyXMLPropertyValues(ServletRequest request) {
		this(request, null, null);
	}
	
	public ServletRequestBodyXMLPropertyValues(ServletRequest request, String prefix, String prefixSeparator) {
		super(XMLWebUtils.getElementsStartingWith(
				request, (prefix != null) ? prefix + prefixSeparator : null));
	}

}

src/main/java/com/test/fw/XMLWebUtils.java

package com.test.fw;

import java.io.IOException;
import java.util.Map;
import java.util.TreeMap;

import javax.servlet.ServletRequest;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.springframework.util.Assert;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * This class offers some enhanced features relating to REST processing
 * XML submitted by extJS component as part of a form submission.
 */
public class XMLWebUtils {
	static class MyDefaultHandler extends DefaultHandler {
		private String tempVal;
		
		final private Map<String, String> params;
		private String rootElement;
		private String prefix;
		
		public MyDefaultHandler(Map<String, String> map, String prefix) {
			this.params = map;
			this.prefix = prefix;
		}
		
		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) throws SAXException {
			if(qName == null) {
				return;
			}
			
			if(rootElement == null) {
				rootElement = qName;
			} 
			
		}
	    
		public void characters(char[] ch, int start, int length) throws SAXException {
	        tempVal = new String(ch,start,length);
	    }
	    
		@Override
		public void endElement(String uri, String localName, String qName)
				throws SAXException {

			if(qName.equalsIgnoreCase(rootElement)) {
				rootElement = null;
			} else {
				params.put(qName, tempVal);
				tempVal = null;
			}
		}
	}
	
	/**
	 * This method creates a map of element values submitted by the extJS component.
	 * The extjs component has been set to submit an xml representation of the data. 
	 * This method uses SAX API that is built into the JDK 1.5 and above.
	 * 
	 * @param request
	 * @param prefix
	 * @return
	 */
	public static Map<String, String> getElementsStartingWith(ServletRequest request, String prefix) {
		Assert.notNull(request, "Request must not be null");
		
		Map<String, String> params = new TreeMap<String, String>();

		if (prefix == null) {
			prefix = "";
		}
        //get a factory
        SAXParserFactory spf = SAXParserFactory.newInstance();
        try {
            SAXParser sp = spf.newSAXParser();
            sp.parse(request.getInputStream(), new MyDefaultHandler(params, prefix));
        }catch(SAXException se) {
            se.printStackTrace();
        }catch(ParserConfigurationException pce) {
            pce.printStackTrace();
        }catch (IOException ie) {
            ie.printStackTrace();
        }
		return params;
	}
}

Configure SpringMVC to use the Custom Annotation Method Handler Adapter

The AnnotationMethodHandlerAdapters is configured using the spring-servlet.xml file. You can only have one HandlerAdapter per URL mapping. We will configure an alternate HandlerAdapter using another mapping.

Modify the web.xml and add an additional DispatcherServlet.

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>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>springdata</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>springdata</servlet-name>
        <url-pattern>/appdata/*</url-pattern>
    </servlet-mapping>
 
</web-app>

The following is the configuration file for the new DispatcherServlet we defined above. Spring MVC follows a standard naming convention with reading xml configurations. If the servlet name is “springdata” then the springdata-servlet.xml file in the WEB-INF folder will be read. The below configuration file is similar to the original one except we have swapped out the HandlerAdapter for “com.test.CustomAnnotationMethodHandlerAdapter”.

src/main/webapp/WEB-INF/springdata-servlet.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
        class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
    <bean
        class="com.test.fw.CustomAnnotationMethodHandlerAdapter" />
 
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>
 
</beans>

Setup the ExtJS Front End JavaScript/HTML

Lastly we will set-up the extJS Front End.

The following enhances the extJS editor grid to display validation failures. Thanks to Oskar Johansson for providing a excellent solution.

src/main/webapp/gridValidation.js

// Oskar Johansson, http://onkelborg.com
    activateGridValidation = function (grid) {
        var store = grid.getStore();
        var mapRecordToRequestJsonData = [];
        var mapRecordToRequestRecord = [];
        var storeOnBeforeSave = function (store, data) {
            mapRecordToRequestJsonData = [];
            mapRecordToRequestRecord = [];
        };
        var storeOnBeforeWrite = function (dataProxy, action, rs, params) {
            mapRecordToRequestJsonData.push(params.jsonData);
            mapRecordToRequestRecord.push(rs);
        };
        var storeOnException = function (dataProxy, type, action, options, response, arg) {
            if (action == "create" || action == "update") {
                var erroneousObject = mapRecordToRequestRecord[mapRecordToRequestJsonData.indexOf(options.jsonData)];
                var rowIndex = store.indexOf(erroneousObject);
                var colModel = grid.getColumnModel();
                var gridView = grid.getView();
                var errorsInner;
                var errors = response.raw ? response.raw.errors : ((errorsInner = Ext.decode(response.responseText)) ? errorsInner.errors : null);
                var editableColumns = [];
                for (var i = 0; i < colModel.getColumnCount(); i++) {
                    var column = colModel.getColumnById(colModel.getColumnId(i));
                    if (column.getCellEditor(rowIndex)) {
                        editableColumns.push(colModel.getDataIndex(i));
                    }
                }
                if (errors) {
                    var erroneousColumns = [];
                    for (var x in errors) {
                        erroneousColumns.push(x);
                    }
                    for (var i = 0; i < erroneousColumns.length; i++) {
                        editableColumns.splice(editableColumns.indexOf(erroneousColumns[i]), 1);
                    }
                    for (var i = 0; i < erroneousColumns.length; i++) {
                        var errKey = erroneousColumns[i];
                        var colIndex = colModel.findColumnIndex(errKey);
                        var msg = errors[errKey];
                        var cell = gridView.getCell(rowIndex, colIndex);
                        var cellElement = Ext.get(cell);
                        var cellInnerElement = cellElement.first();
                        cellInnerElement.addClass("x-form-invalid");
                        cellInnerElement.set({ qtip: msg });
                    }
                }
                for (var i = 0; i < editableColumns.length; i++) {
                    var colIndex = colModel.findColumnIndex(editableColumns[i]);
                    var cell = gridView.getCell(rowIndex, colIndex);
                    var cellElement = Ext.get(cell);
                    var cellInnerElement = cellElement.first();
                    cellInnerElement.removeClass("x-form-invalid");
                    cellInnerElement.set({ qtip: "" });
                }
            }
        };
        store.proxy.on("exception", storeOnException);
        store.proxy.on("beforewrite", storeOnBeforeWrite);
        store.on("beforesave", storeOnBeforeSave);
        grid.on("destroy", function () {
            store.proxy.un("exception", storeOnException);
            store.proxy.un("beforewrite", storeOnBeforeWrite);
            store.un("beforesave", storeOnBeforeSave);
        });
    }; 

The next file contains the JavaScript code that sets up the editor grid panel.
src/main/webapp/restful.js

MyApp={};
MyApp.getContext = function() {
	var base = document.getElementsByTagName('base')[0];
	if (base && base.href && (base.href.length > 0)) {
		base = base.href;
	} else {
		base = document.URL;
	}
	return base.substr(0, base.indexOf("/", base.indexOf("/", base.indexOf("//") + 2) + 1));
};

var App = new Ext.App({});
 
var proxy = new Ext.data.HttpProxy({
	url: MyApp.getContext() + '/appdata/itemresource'
});

var reader = new Ext.data.JsonReader({
    totalProperty: 'total',
    successProperty: 'success',
    idProperty: 'itemId',
    root: 'data',
    record: 'item',
    messageProperty: 'message',  // <-- New "messageProperty" meta-data
    fields : [
              {name: 'itemId'},
              {name: 'name', allowBlank: false},
              {name: 'description', allowBlank: true},
              {name: 'stockQty', allowBlank: true},
              {name: 'color', allowBlank: true}
    ]
});

// The new DataWriter component.
var writer = new Ext.data.XmlWriter({
    encode : false,
    writeAllFields : true,
    xmlEncoding : "UTF-8"
});

// Typical Store collecting the Proxy, Reader and Writer together.
var store = new Ext.data.Store({
    id: 'item',
    restful: true,     // <-- This Store is RESTful
    proxy: proxy,
    reader: reader,
    writer: writer    // <-- plug a DataWriter into the store
});

// load the store
store.load();

// The following is necessary to map the Record Fields to the grid columns.
var userColumns =  [
    {header: "Item Id", width: 40, sortable: true, dataIndex: 'itemId'},
    {header: "Item Name", width: 100, sortable: true, dataIndex: 'name', editor: new Ext.form.TextField({})},
    {header: "Description", width: 100, sortable: true, dataIndex: 'description', editor: new Ext.form.TextField({})},
    {header: "Quantity", width: 100, sortable: true, dataIndex: 'stockQty', editor: new Ext.form.TextField({})},
    {header: "Color", width: 100, sortable: true, dataIndex: 'color', editor: new Ext.form.TextField({})}
];

Ext.onReady(function() {
    Ext.QuickTips.init();

    // use RowEditor for editing
    var editor = new Ext.ux.grid.RowEditor({
        saveText: 'Save'
    });

    // Create a typical GridPanel with RowEditor plugin
    var userGrid = new Ext.grid.GridPanel({
        renderTo: 'user-grid',
        iconCls: 'icon-grid',
        frame: true,
        title: 'Fun with RESTful CRUD',
        height: 300,
        store: store,
        plugins: [editor],
        columns : userColumns,
    	enableColumnHide: false,
        tbar: [{
        	id: 'addBtn',
            text: 'Add',
            iconCls: 'silk-add',
            handler: onAdd
        }, '-', {
        	id: 'deleteBtn',
            text: 'Delete',
            iconCls: 'silk-delete',
            handler: onDelete
        }, '-'],
        viewConfig: {
            forceFit: true
        }
    });

    function onAdd(btn, ev) {
        var u = new userGrid.store.recordType({
            first : '',
            last: '',
            email : ''
        });
        editor.stopEditing();
        userGrid.store.insert(0, u);
        editor.startEditing(0);
    }

    // Disable the buttons when editing.
    editor.addListener('beforeedit', function(roweditor, rowIndex){
        Ext.getCmp('addBtn').disable();
        Ext.getCmp('deleteBtn').disable();
    });
    editor.addListener('afteredit', function(roweditor, rowIndex){
        Ext.getCmp('addBtn').enable();
        Ext.getCmp('deleteBtn').enable();
    });
    editor.addListener('canceledit', function(roweditor, rowIndex){
    	// get fresh records from the database.
        userGrid.store.reload({});
        Ext.getCmp('addBtn').enable();
        Ext.getCmp('deleteBtn').enable();
    });     

    function onDelete() {
        var rec = userGrid.getSelectionModel().getSelected();
        if (!rec) {
            return false;
        }
        userGrid.store.remove(rec);
    }
    activateGridValidation(userGrid);
});

The following file pulls everything together.
src/main/webapp/grid.html

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<title>RESTful Store Example</title>

<!-- extJS JavaScript files -->
<script type="text/javascript"
    src="http://dev.sencha.com/deploy/ext-3.3.1/adapter/ext/ext-base-debug.js">
</script> 
<script type="text/javascript"
    src="http://dev.sencha.com/deploy/ext-3.3.1/ext-all-debug.js">
</script>
<!-- extJS example JavaScript files -->
<script type="text/javascript"
	src="http://dev.sencha.com/deploy/ext-3.3.1/examples/shared/extjs/App.js"></script>
<script type="text/javascript"
	src="http://dev.sencha.com/deploy/ext-3.3.1/examples/ux/RowEditor.js"></script>

<!-- My JavaScript Files -->
<script type="text/javascript" src="restful.js"></script>
<script type="text/javascript" src="gridValidation.js"></script>
 
<!-- Common Styles -->
<link rel="stylesheet" type="text/css"
    href="http://dev.sencha.com/deploy/ext-3.3.1/resources/css/ext-all.css"/>
<link rel="stylesheet" type="text/css" href="restful.css" />
<!-- Example Styles -->
<link rel="stylesheet"
	href="http://dev.sencha.com/deploy/ext-3.3.1/examples/ux/css/RowEditor.css" />
<link rel="stylesheet" type="text/css"
	href="http://dev.sencha.com/deploy/ext-3.3.1/examples/shared/examples.css" />
<link rel="stylesheet" type="text/css"
	href="http://dev.sencha.com/deploy/ext-3.3.1/examples/shared/icons/silk.css" />

</head>
<body>
<h1>RESTful Editor Grid with Validation</h1>
<h3>About This Example:</h3>
    <div><ul>
    <li>Uses HyperSQL, IBATIS, Spring</li>
    </ul></div>

<div class="container" style="width:500px">
    <div id="user-grid"></div>
</div>
<h3>Tips:</h3>
    <div><ul>
    <li>1. Use Firebug to inspect RESTful operations</li>
    <li>2. Validation rules are modified by editing src/main/webapp/WEB-INF/validation.xml</li>
    </ul></div>

<h3>Steps to Add a new Column:</h3>
    <div><ul>
    <li>1. Add column to the database src/main/resources/ddl.xml</li>
    <li>2. Add column to the grid located in restful.js</li>
    <li>3. Add the new attribute to Item.java Bean (create getters/setters as well)</li>
    <li>4. Change IBATIS config file located in src/main/resources/com/test/ItemDataManager.xml</li>
    <li>5. Optionally Add column validation to src/main/webapp/WEB-INF/validation.xml</li>
    </ul></div>

</body>
</html>

Modify the index.html file and add the link to the above page.

The index.html should look like this.

src/main/webapp/index.html

<html>
<body>
<h3><a href="http://localhost:8080/crud/app/item">Part I - Simple form.</a></h3>
<h3><a href="form.html">Part II - Click here for the ExtJS Form</a></h3>
<h3><a href="grid.html">Part III -Click here for the ExtJS Editor Grid</a></h3>
</body>
</html>

Start Jetty Servlet Engine

mvn clean compile jetty:run

Test the application

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

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.

Create 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.

Note: If you have any suggestions to improve the code please send me a comment below. I read every comment and appreciate constructive feedback.

07
Apr
11

Extracting and Tokenizing HTTP Path Variables

This page describes how to extract and tokenizing HTTP path variable(s).

Background

URL Path is often used by RESTful applications to pass scoping information. Applications often parse this information and use it to retrieve data from the server-side.

Modern frameworks like Spring MVC 3 and The RESTlet frameworks have means of extracting tokens from HTTP Path. However if you aren’t using those frameworks then the information on this page will be helpful.

The utility class described on this page will allow you to extract tokens present in URL paths. For example “item/{itemId}/subcomponent/{subComponentId}”

Requirements

  • Maven 2
  • Spring 2.5.6 or above
  • Basic understanding of Struts MVC

Start a new project

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

groupId: com.test
artifactId: pathVarTest

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

cd to the project base folder.

cd pathVarTest

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/webapp/WEB-INF/jsp

Modify the project Configuration

The pom.xml file should look something like this.

<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>pathVarTest</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>pathVarTest 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.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>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-servlet_2.5_spec</artifactId>
            <version>1.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.1.2</version>
        </dependency>
        <dependency>
            <groupId>taglibs</groupId>
            <artifactId>standard</artifactId>
            <version>1.1.2</version>
        </dependency>    
  </dependencies>
  <build>
    <finalName>pathVarTest</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>
                <configuration>
                    <scanIntervalSeconds>2</scanIntervalSeconds>
                    <stopKey>s</stopKey>
                </configuration>
            </plugin>
        </plugins>
  </build>
</project>

PathUtil

The following is where the URL path gets tokenized. It is a static utility class that could be used from a Spring MVC controller or a HTTPServlet Class.

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>();
		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;
	}
}

Spring MVC Controller

The following controller class responds to the following URL’s

  • item/{itemId}
  • item/{itemId}/subcomponent/{subComponentId}

src/main/java/com/test/ItemController.java

package com.test;
 
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
public class ItemController {
 
    @RequestMapping(value="/item/*", method = RequestMethod.GET)
    public String itemInfo(HttpServletRequest request) {
    	final String pattern = "/item/*";
		final String[] names = {"itemId"};
		Map<String, String> pathMap = PathUtil.getPathVariables(pattern, names, request
				.getPathInfo());
   	
		request.setAttribute("pathMap", pathMap);
    	
        return "index";
    }
    @RequestMapping(value="/item/*/subcomponent/*", method = RequestMethod.GET)
    public String subComponentItemInfo(HttpServletRequest request) {
    	final String pattern = "/item/*/subcomponent/*";
		final String[] names = {"itemId", "subComponentId"};
		Map<String, String> pathMap = PathUtil.getPathVariables(pattern, names, request
				.getPathInfo());
   	
		request.setAttribute("pathMap", pathMap);
    	
        return "index";
    }
}

JSP

The following JSP is used to display the Hash Map that contains the results. I haven’t put any work in making it look pretty. Its just functional.

src/main/webapp/WEB-INF/jsp/index.jsp

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=iso-8859-1" />
<meta http-equiv="Content-Language" content="en-us" />
 
</head>
<body>
<h3>This are the tokens from the URL Path Info String</h3>
<h3><c:out  value="${pathMap}"/></h3>
</body>
</html>

Web App 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>
    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>

Spring MVC Configuration file

This is a pretty standard Spring MVC configuration file.

src/main/webapp/WEB-INF/spring-servlet.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
        class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
    <bean
        class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
 
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean> 
</beans>

Test the application

Start the Jetty Servlet Engine to test the application with 1 scoping variable.

  1. Open a command line window and navigate to the project base directory.
  2. Type the following at the command line:
    mvn clean compile jetty:run
    
  3. Navigate to the following URL: http://localhost:8080/app/item/235
  4. You should see the next page which prints “{itemId=235}”.

Test the application with multiple scoping variables.

  1. Navigate to the following URL: http://localhost:8080/app/item/123/subcomponent/456
  2. You should see the next page which prints “{subComponentId=456, itemId=235}”.

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 project’s “target/” folder…

That’s all for now!

20
Mar
11

ExtJS Ajax Form Submission

This page describes the process of creating a extJS HTML form that submits data to a server side resource via Ajax. This page will also cover the validation failure scenario where custom error messages are displayed next to components that have invalid data. This page “simulates” the server side resource, therefore it is not Java / JEE specific and does not require server side programming to implement.

Background

The most popular methods for HTML form submission is to use either “GET” or “POST”.

The “POST” method causes the fields to be encoded into the body of the request. Although this is the preferred method to submit forms with large amount of data, when users arrive on the next page and click refresh they are presented with an annoying message (see below).

Browser Refresh message in Firefox: “To display this page, Firefox must send information that will repeat any action (such as a search or order confirmation) that was performed earlier.”

Browser Refresh message in Internet Explorer 6: “The page can not be refreshed without resending the information. Click Retry to send the information again or click Cancel to return to the page that you were trying to view.”

To most users these messages are confusing/annoying and can result in corrupted data. Imagine a user unknowingly submitting an order twice by clicking refresh on the order confirmation page.

The “GET” method causes the form fields to be encoded into the URL get string. For most large forms or forms that contain binary data this is not an option.

One of the benefits of using extJS for form submission is that with the help of Ajax it allows for the possibility of submitting data to the server in a separate “flow” of execution. Form submissions using Ajax don’t cause the page to reload. The display of the next page is not tied to the submission of the form. This provides the user with a richer client experience and avoids the mess caused by classic HTML form submission.

Requirements

  • Basic Understanding of extJS
  • HTTP Server

Procedure

The following html draws a form on the page. On the bottom of the form there is a Load and Save buttons.

The save button calls “returnSuccess.do”. In order to see validation error messages just replace “returnSuccess.do” with “returnFailure.do”.

index.html

<html>
<head>

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

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

<script type="text/javascript">

function buildWindow() {

    Ext.QuickTips.init();

    // turn on validation errors beside the field globally
    Ext.form.Field.prototype.msgTarget = 'side';

    var bd = Ext.getBody();

    var fs = new Ext.FormPanel({
        labelWidth: 75, // label settings here cascade unless overridden    
        frame: true,
        title:'Form 2 - Ajax Enabled',
        bodyStyle:'padding:5px 5px 0',
        width: 350,
        defaults: {width: 230},
        waitMsgTarget: true,
        defaultType: 'textfield',        
        
        items: [{
                fieldLabel: 'First Name',
                name: 'firstName',
                allowBlank:false
            },{
                fieldLabel: 'Last Name',
                name: 'lastName'
            },{
                fieldLabel: 'Company',
                name: 'company'
            }, {
                fieldLabel: 'Email',
                name: 'email',
                vtype:'email'
            }, new Ext.form.TimeField({
                fieldLabel: 'Time',
                name: 'time',
                minValue: '8:00am',
                maxValue: '6:00pm'
            })
        ],        

	});
	
	
	// simple button add
	fs.addButton('Load', function(){
	    fs.getForm().load({url:'data.do', waitMsg:'Loading'});
	});

	var onSuccessOrFail = function(form, action) {
		var result = action.result;
		
		if(result.success) {
			Ext.MessageBox.alert('Success', 'Success!');
		} else {
			Ext.MessageBox.alert('Failure', 'Failure!');
		}
		
	}

	// explicit add
	var submit = fs.addButton({
	    text: 'Submit',
	    disabled:true,
	    handler: function(){
	        fs.getForm().submit({
	        	url:'returnSuccess.do', 
//	        	url:'returnFailure.do', 
	        	waitMsg:'Saving Data...', 
	        	submitEmptyText: false,
	        	success : onSuccessOrFail,
	        	failure : onSuccessOrFail
	        });
	    }
	});

	fs.render('form-first');

	fs.on({
	    actioncomplete: function(form, action){
	        if(action.type == 'load'){
	            submit.enable();
	        }
	    }
	});

}
Ext.onReady(buildWindow);
</script>
 
</head>
<body>

<div id="form-first"></div>

</body>
</html>

Data

The following files represent simulated responses from the server side resource.

Load Data

The “data.do” represents the name, company and email information for a single person. It is retrieved from the server when the user clicks the load button. URL specified in “index.html” line 61 (highlighted) above.

data.do

{
    success : true,
    data    : {
        firstName : 'Jack',
        lastName  : 'Slocum',
        company   : 'Slocum LLC',
        email     : 'jack@slocum.com',
        time      : '9:45 AM'
    }
}

Save Data

When the user clicks the “save” button the URL specified in “index.html” line 81 (highlighted) will be called and the extJS component is expecting the following response from the server.

returnSuccess.do

{success: true, msg : 'Thank you for your submission.'}

Save Data with Failure

In the case the server side resource returns validation failures the extJS component is expecting them in the following format from the server. The failure messages displayed below display next to their prospective fields.

returnFailure.do

{
   success : false,
   msg     : 'This is an example error message',
   errors  : {
      firstName : 'Sample Error for First Name',
      lastName  : 'Sample Error for Last Name'
   }
}

Run the Example

Since we are making Ajax requests a Web Server is required. Save all the files and navigate to index.html using a web browser.

Click on Load to load the data. Click on Save to view the success message. In order to view the validation failure messages replace the URL on line 81 with “returnFailure.do”.

Summary

The example described on this page is very simple. However it does cover some of the basics of loading and submitting an extJS form using an Ajax.

The chances of this example above being used on an actual page are slim. Reason??? What happens after a form is submitted? The answer is by default… NOTHING…

After thinking about the above I gathered my thoughts and am in the process of writing Part II of this page which covers “User Interactions” with ExtJS enabled web applications. (soon to come).

That’s all for now.




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

Join 77 other followers

November 2017
S M T W T F S
« Oct    
 1234
567891011
12131415161718
19202122232425
2627282930  

Blog Stats

  • 841,714 hits