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.

Advertisements

11 Responses to “RESTful CRUD using extJS”


  1. 1 Rey
    June 14, 2011 at 5:33 am

    Hello Sir!

    What version of ExtJS you are using for this tutorial?

  2. February 28, 2013 at 6:18 am

    thank you very much … this helps me to solve error in my application. :)

  3. 5 Doug
    February 28, 2013 at 10:33 am

    Excellent tutorial!!! Very clear. Thank you for sharing.

  4. October 6, 2015 at 11:16 am

    Please you can share the user and password to download from svn ? thanks so much.


Leave a Reply

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

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s


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

Join 77 other followers

April 2011
S M T W T F S
« Mar   May »
 12
3456789
10111213141516
17181920212223
24252627282930

Blog Stats

  • 846,580 hits

%d bloggers like this: