Posts Tagged ‘Spring MVC

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/

20
Apr
11

ExtJS + Spring MVC validation using commons-validator

This page describes the process of validating a extJS form using Spring MVC and the commons-validator framework. 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-springmvc-form-validation/. Get the project and skip to the Test The Application section of this page.

Background

This page is a re-implementation of extjs-form-validation-using-commons-validator. This page uses Spring MVC instead of Struts.

Commons validation is a mature project that’s been out there for a while now. It became popular when it was introduced as part of the Struts framework. Commons validation allows developers to specify validation rules inside XML files, thus allowing them to be applied uniformly across the site. In addition the same set of rules can be used to generate both client side and server side validation thus reducing maintenance cost.

In the interest of brevity, this page will only cover the server side validation.

Using commons-validator by itself doesn’t give you much benefit. All you get is the ability to specify rules and validation logic that returns whether the validation passed or failed.

You are left on your own to:

  1. Write your own code to iterate thru the form fields and collect the failure messages
  2. Write a servlet to handle requests over HTTP and present the failure messages back to the client
  3. Internationalization Support

Although its not that much work, when you start implementing it you quickly realize that this boiler plate code is something that a framework should provide.

This boiler plate code has been written years ago by the makers of the Struts framework. And later the Folks over at springsource wrote a validator module for it allowing the Spring framework to use validation.

By extending the Spring MVC framework slightly, we can use it to validate forms submitted by extJS components.

Spring MVC comes pre-packaged with the “form:errors” tag. Upon validation failures the Spring MVC controller should returns the user back to the “input” page and the input page contains a “form:errors” tag that displays the failed validation along with a description of the failure in HTML format.

The Approach

The approach presented here focuses on extending the “form:errors” tag to return JSON instead of html. The JSON is parsed by the extJS formPanel and the errors are presented to the user.

On success the extJS formPanel expects:

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

On failure the extJS formPanel expects:

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

Note:
Some of the code on this page are not core to the understanding “approach”, for this reason the code will initially be collapsed.

Requirements

Since this page is about integrating 3 different technologies together a basic understanding of Spring MVC, commons-validation, and extJS is assumed. If any of these technologies are not clear then please visit some of my other tutorial pages.

  1. Maven 2
  2. Basic Understanding of extJS, Spring MVC and commons-validator
  3. Successful completion of all examples in the the following page.

Continue with an existing project

We will continue with an existing project that implements the Model layer. If you have not done so already please visit the following page for more information.

Add the validation libraries to the pom.xml

pom.xml

		<dependency>
		    <groupId>org.springmodules</groupId>
		    <artifactId>spring-modules-validation</artifactId>
		    <version>0.8</version>
		</dependency>
		<dependency>
		    <groupId>commons-validator</groupId>
		    <artifactId>commons-validator</artifactId>
		    <version>1.1.4</version>
		</dependency>       

Project Resources

The following file contains the messages to display to the user. Please note that there are a series of “typeMismatch” lines. These are for the Spring MVC data binder. It seems that in Spring MVC the Binding occurs before the validation. Therefore if these entries are not made in the property file, the system would display cryptic error messages about the binding attempt.

src/main/resources/application.properties

errors.required={0} is required.
errors.minlength={0} can not be less than {1} characters.
errors.maxlength={0} can not be greater than {1} characters.
errors.invalid={0} is invalid.
 
errors.byte={0} must be a byte.
errors.short={0} must be a short.
errors.integer={0} must be an integer.
errors.long={0} must be a long.
errors.float={0} must be a float.
errors.double={0} must be a double.
 
errors.date={0} is not a date.
errors.range={0} is not in the range {1} through {2}.
errors.creditcard={0} is an invalid credit card number.
errors.email={0} is an invalid e-mail address.

# The following are used during Spring MVC Data Binding
typeMismatch=invalid field 
typeMismatch.int={0} must be an integer. 
typeMismatch.java.lang.Integer={0} must be an integer. 
typeMismatch.java.util.Date={0} must be a Date.

item.name=Item Name
item.description=Item Description
item.color=Item Color
item.stockQty=Item Quantity
item.color.must.be=Item color must be blue.

Add the following 2 beans to the existing spring configuration file.

src/main/resources/applicationContext.xml

	<bean id="validatorFactory"
	      class="org.springmodules.validation.commons.DefaultValidatorFactory">
	<property name="validationConfigLocations">
	    <list>
	      <value>/WEB-INF/validation.xml</value>
	      <value>/WEB-INF/validator-rules.xml</value>
	    </list>
	  </property>
	</bean>    
	<bean id="beanValidator" class="org.springmodules.validation.commons.DefaultBeanValidator">
	<property name="validatorFactory" ref="validatorFactory"/>
	</bean>    
 
 <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">  
     <property name="basenames">  
         <value>application</value>  
     </property>  
 </bean>  

Extending SpringMVC tag libraries

Create the fw directory

mkdir -p src/main/java/com/test/fw

The Spring MVC framework does not return validation failures in JSON format. Therefore we need to modify the “form:errors” tag to return errors in a format that can be recognized by extJS. We will be overriding the default behaviour of the tags using the following classes.

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

package com.test.fw;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;

import org.springframework.beans.PropertyAccessor;
import org.springframework.web.servlet.tags.form.FormTag;
import org.springframework.web.servlet.tags.form.TagWriter;

/**
 * Extend the Spring MVC Form tag to suppress any output.
 */
public class JSFormTag extends FormTag {
	private static final long serialVersionUID = 1L;

	private String previousNestedPath;

	@Override
	protected int writeTagContent(TagWriter tagWriter) throws JspException {
		
		// Expose the form object name for nested tags...
		String modelAttribute = resolveModelAttribute();
		this.pageContext.setAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, modelAttribute, PageContext.REQUEST_SCOPE);
		this.pageContext.setAttribute(COMMAND_NAME_VARIABLE_NAME, modelAttribute, PageContext.REQUEST_SCOPE);

		// Save previous nestedPath value, build and expose current nestedPath value.
		// Use request scope to expose nestedPath to included pages too.
		this.previousNestedPath =
				(String) this.pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
		this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME,
				modelAttribute + PropertyAccessor.NESTED_PROPERTY_SEPARATOR, PageContext.REQUEST_SCOPE);

		return EVAL_BODY_INCLUDE;
	}

	/**
	 * Closes the '<code>form</code>' block tag and removes the form object name
	 * from the {@link javax.servlet.jsp.PageContext}.
	 */
	public int doEndTag() throws JspException {
		//this.tagWriter.endTag();
		this.pageContext.removeAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
		this.pageContext.removeAttribute(COMMAND_NAME_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
		if (this.previousNestedPath != null) {
			// Expose previous nestedPath value.
			this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, this.previousNestedPath, PageContext.REQUEST_SCOPE);
		}
		else {
			// Remove exposed nestedPath value.
			this.pageContext.removeAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
		}
		return EVAL_PAGE;
	}

	/**
	 * Clears the stored {@link TagWriter}.
	 */
	public void doFinally() {
		super.doFinally();
		this.previousNestedPath = null;
	}

	
}

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

package com.test.fw;

import java.util.Map;

import javax.servlet.jsp.PageContext;

import org.springframework.web.servlet.support.JspAwareRequestContext;

/**
 * Sole purpose of this class is to force the JSPAwareRequestContext to
 * reveal the model object.
 */
public class MyRequestContext extends JspAwareRequestContext {
	@Override
	public Object getModelObject(String modelName) {
		return super.getModelObject(modelName);
	}

	public MyRequestContext(PageContext pageContext, Map model) {
		super(pageContext, model);
	}

	public MyRequestContext(PageContext pageContext) {
		super(pageContext);
	}
	
}

The following class is core to the understanding of this page. This class is a replacement for the form:errors tag. Instead of outputting HTML to display the errors to the user it show the errors in JSON format.

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

package com.test.fw;

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

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;

import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.beans.PropertyAccessor;
import org.springframework.web.servlet.tags.NestedPathTag;
import org.springframework.web.servlet.tags.RequestContextAwareTag;

public class JSErrorsTag extends RequestContextAwareTag {
	private static final long serialVersionUID = 1L;
	
	protected JSBindStatus bindStatus;
	protected String path;
	
	protected static final String NESTED_PATH_VARIABLE_NAME = NestedPathTag.NESTED_PATH_VARIABLE_NAME;
	public static final String MESSAGES_ATTRIBUTE = "messages";

	
	@Override
	protected int doStartTagInternal() throws Exception {
		SafeWriter safeWriter = new SafeWriter(pageContext);
		JSONObject jsonReturn = new JSONObject();
		JSONObject jsonErrors = new JSONObject();
		 

		
		Map<String, String> errorMessages = getJSBindStatus().getErrorMessagesMap();
		JSONObject jsonData = new JSONObject(getJSBindStatus().getModel());

		boolean successFlag = false;
		if(errorMessages==null || errorMessages.size() == 0) {
			successFlag = true;
		} else {
			jsonReturn.put("errors", jsonErrors);			
		}
		try {
			jsonReturn.put("data", jsonData);
			jsonReturn.put("msg", successFlag ? "Success!" : "There was a validation failure." );
			jsonReturn.put("success", successFlag);
		} catch (JSONException e) {
			throw new RuntimeException(e);
		}
		
		for(Map.Entry<String, String> error : errorMessages.entrySet()) {
			try {
				jsonErrors.append(error.getKey(), error.getValue());
			} catch (JSONException e) {
				throw new RuntimeException(e);				
			}
		}
	
		safeWriter.append(jsonReturn.toString());

		return 0;
	}	
	
	public void doFinally() {
		super.doFinally();
		this.pageContext.removeAttribute(MESSAGES_ATTRIBUTE,
				PageContext.PAGE_SCOPE);
		this.bindStatus = null;
	}
	
	/**
	 * Get the value of the nested path that may have been exposed by the
	 * {@link NestedPathTag}.
	 */
	protected String getNestedPath() {
		return (String) this.pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
	}
	
	protected JSBindStatus getJSBindStatus() throws JspException {
		if (this.bindStatus == null) {
			// HTML escaping in tags is performed by the ValueFormatter class.
			String nestedPath = getNestedPath();
			String pathToUse = (nestedPath != null ? nestedPath + getPath() : getPath());
			if (pathToUse.endsWith(PropertyAccessor.NESTED_PROPERTY_SEPARATOR)) {
				pathToUse = pathToUse.substring(0, pathToUse.length() - 1);
			}

			//TODO: is this the correct way? check the original code.
			MyRequestContext context = new MyRequestContext(pageContext);

			this.bindStatus = new JSBindStatus(context, pathToUse, false);
			
		}
		return this.bindStatus;
	}

	public String getPath() {
		return path;
	}

	public void setPath(String path) {
		this.path = path;
	}	
	
	private static class SafeWriter {

		private PageContext pageContext;

		private Writer writer;

		public SafeWriter(PageContext pageContext) {
			this.pageContext = pageContext;
		}

		public SafeWriter append(String value) throws JspException {
			try {
				getWriterToUse().write(String.valueOf(value));
				return this;
			}
			catch (IOException ex) {
				throw new JspException("Unable to write to JspWriter", ex);
			}
		}

		private Writer getWriterToUse() {
			return (this.pageContext != null ? this.pageContext.getOut() : this.writer);
		}
	}	
	
}

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

package com.test.fw;

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

import org.springframework.context.NoSuchMessageException;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.servlet.support.RequestContext;

/**
 * This is the BindStatus class simplified.
 */
public class JSBindStatus {

	protected final Errors errors;
	protected final Object model;
	
	protected List<FieldError> objectErrors;
	protected final String expression;

	protected Map<String,String> errorMessagesMap;
	protected RequestContext requestContext;
	protected String path;
	protected boolean htmlEscape;
	
	public JSBindStatus(MyRequestContext requestContext, String path,
			boolean htmlEscape) throws IllegalStateException {

		this.requestContext = requestContext;
		this.path = path;
		this.htmlEscape = htmlEscape;

		// determine name of the object and property
		String beanName = null;
		int dotPos = path.indexOf('.');
		if (dotPos == -1) {
			// property not set, only the object itself
			beanName = path;
			this.expression = null;
		}
		else {
			beanName = path.substring(0, dotPos);
			this.expression = path.substring(dotPos + 1);
		}

		this.model = requestContext.getModelObject(beanName);

		this.errors = requestContext.getErrors(beanName, false);
					
		if (this.errors != null) {
			// Usual case: A BindingResult is available as request attribute.
			// Can determine error codes and messages for the given expression.
			// Can use a custom PropertyEditor, as registered by a form controller.
			if (this.expression != null) {
				if ("*".equals(this.expression)) {
					this.objectErrors = this.errors.getAllErrors();
				}
			}
			else {
				this.objectErrors = this.errors.getGlobalErrors();
			}
			//initErrorCodes();
		}			
	}

	public Map<String, String> getErrorMessagesMap() {
		initErrorMessages();
		return errorMessagesMap;
	}
	
	/**
	 * Extract the error messages from the ObjectError list.
	 */
	private void initErrorMessages() throws NoSuchMessageException {
		if (this.errorMessagesMap == null) {
			this.errorMessagesMap = new HashMap<String,String>();
			if(objectErrors != null) {
				for (FieldError error : objectErrors) {
					String message = this.requestContext.getMessage(error, this.htmlEscape);
					errorMessagesMap.put(error.getField(), message);
				}				
			}
		}
	}

	public Object getModel() {
		return model;
	}	
}

Custom Tag library descriptor

The following is the tag library configuration file. Typically this would be packaged into a re-usable jar file but for the purposes of this tutorial I will put it into the web-inf folder.

src/main/webapp/WEB-INF/rest-mvc.tld

<taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-
jsptaglibrary_2_0.xsd"
    version="2.0">
<tlib-version>1.0</tlib-version>
<uri>http://test.com/jsrestlib</uri>

	<tag>
		<name>form</name>
		<tag-class>com.test.fw.JSFormTag</tag-class>
		<body-content>JSP</body-content>
		<description>Renders an HTML 'form' tag and exposes a binding path to inner tags for binding.</description>
		<attribute>
			<name>id</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute</description>
		</attribute>
		<attribute>
			<name>name</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute - added for backwards compatibility cases</description>
		</attribute>
		<attribute>
			<name>htmlEscape</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Enable/disable HTML escaping of rendered values.</description>
		</attribute>
		<attribute>
			<name>cssClass</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Equivalent to "class" - HTML Optional Attribute</description>
		</attribute>
		<attribute>
			<name>cssStyle</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Equivalent to "style" - HTML Optional Attribute</description>
		</attribute>
		<attribute>
			<name>lang</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute</description>
		</attribute>
		<attribute>
			<name>title</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute</description>
		</attribute>
		<attribute>
			<name>dir</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute</description>
		</attribute>
		<attribute>
			<name>onclick</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>ondblclick</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmousedown</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmouseup</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmouseover</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmousemove</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmouseout</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onkeypress</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onkeyup</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onkeydown</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<!-- Form specific attributes -->
		<attribute>
			<name>modelAttribute</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Name of the model attribute under which the form object is exposed.
				Defaults to 'command'.</description>
		</attribute>
		<attribute>
			<name>commandName</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Name of the model attribute under which the form object is exposed.
				Defaults to 'command'.</description>
		</attribute>
		<attribute>
			<name>action</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Required Attribute</description>
		</attribute>
		<attribute>
			<name>method</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Optional Attribute</description>
		</attribute>
		<attribute>
			<name>target</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Optional Attribute</description>
		</attribute>
		<attribute>
			<name>enctype</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Optional Attribute</description>
		</attribute>
		<attribute>
			<name>acceptCharset</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Specifies the list of character encodings for input data that is accepted by the server processing this form. The value is a space- and/or comma-delimited list of charset values. The client must interpret this list as an exclusive-or list, i.e., the server is able to accept any single character encoding per entity received.</description>
		</attribute>
		<attribute>
			<name>onsubmit</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onreset</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>autocomplete</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Common Optional Attribute</description>
		</attribute>
		<attribute>
			<name>methodParam</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>The parameter name used for HTTP methods other then GET and POST. Default is '_method'</description>
		</attribute>
	</tag>
 
	<tag>
		<name>jserrors</name>
		<tag-class>com.test.fw.JSErrorsTag</tag-class>
		<body-content>JSP</body-content>
		<description>Renders field errors in an HTML 'span' tag.</description>
		<variable>
			<name-given>messages</name-given>
			<variable-class>java.util.List</variable-class>
		</variable>
		<attribute>
			<name>path</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Path to errors object for data binding</description>
		</attribute>
		<attribute>
			<name>id</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute</description>
		</attribute>
		<attribute>
			<name>htmlEscape</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Enable/disable HTML escaping of rendered values.</description>
		</attribute>
		<attribute>
			<name>delimiter</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Delimiter for displaying multiple error messages. Defaults to the br tag.</description>
		</attribute>
		<attribute>
			<name>cssClass</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Equivalent to "class" - HTML Optional Attribute</description>
		</attribute>
		<attribute>
			<name>cssStyle</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Equivalent to "style" - HTML Optional Attribute</description>
		</attribute>
		<attribute>
			<name>lang</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute</description>
		</attribute>
		<attribute>
			<name>title</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute</description>
		</attribute>
		<attribute>
			<name>dir</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute</description>
		</attribute>
		<attribute>
			<name>tabindex</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Standard Attribute</description>
		</attribute>
		<attribute>
			<name>onclick</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>ondblclick</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmousedown</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmouseup</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmouseover</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmousemove</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onmouseout</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onkeypress</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onkeyup</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>onkeydown</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>HTML Event Attribute</description>
		</attribute>
		<attribute>
			<name>element</name>
			<required>false</required>
			<rtexprvalue>true</rtexprvalue>
			<description>Specifies the HTML element that is used to render the enclosing errors.</description>
		</attribute>
	</tag>

</taglib>

Controller Class

src/main/java/com/test/ItemResourceController.java

package com.test;
 
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

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

import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
public class ItemResourceController {
    private ItemModel itemModel;

    @Qualifier("beanValidator")
    private Validator beanValidator;    

    @RequestMapping(value="/itemresource/*", method = RequestMethod.DELETE)
    public String itemInfoDelete(HttpServletRequest request,
            HttpServletResponse response) {
 
        //TODO: perform validation
        final String pattern = "/itemresource/*";
 
        String[] names = { "itemId" };
 
        Map<String, String> pathMap = PathUtil.getPathVariables(pattern, names,
                request.getPathInfo());
        String itemId = pathMap.get("itemId");
 
        itemModel.deleteItem(Integer.parseInt(itemId));
        JSONObject obj = new JSONObject();
        PrintWriter writer = null;
 
        try {
            obj.put("success", true);
 
            writer = response.getWriter();
            writer.print(obj.toString());
        }  catch (IOException e) {
            throw new RuntimeException(e);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
 
        return null;
    }
 
    @RequestMapping(value="/itemresource", method = RequestMethod.GET)
    public String itemInfo(HttpServletRequest request,
            HttpServletResponse response) {
 
        //TODO: perform validation
        List<Item> items = itemModel.getAllItems(0, 100);
        request.setAttribute("items", items);
 
        List<JSONObject> jsonList = new ArrayList<JSONObject>();
        for (Item item : items) {
            jsonList.add(new JSONObject(item));
        }
 
        JSONObject obj = new JSONObject();
        try {
            obj.put("success", true);
            obj.put("message", "Loaded data");
            obj.put("data", jsonList);
 
            PrintWriter writer;
            writer = response.getWriter();
            writer.print(obj.toString());
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
 
        return null;
    }
 
    @RequestMapping(value="/itemresource/*", method = RequestMethod.PUT)
    public String updateItem(@ModelAttribute Item item, BindingResult result) {
 
        beanValidator.validate(item, result);

        if(result.hasErrors()) {        	
        	return "response";
        }
                
        itemModel.updateItem(item);
        
        return "response";
    }
 
    @RequestMapping(value="/itemresource",method = RequestMethod.POST)
    public String createItem(@ModelAttribute Item item, BindingResult result) {
 
        beanValidator.validate(item, result);
        System.out.println(item);

        if(result.hasErrors()) {        	
        	return "response";
        }
        
        itemModel.createItem(item);
        
        return "response";
    }
 
    public ItemModel getItemModel() {
        return itemModel;
    }
 
    @Required
    @Autowired
    public void setItemModel(ItemModel itemModel) {
        this.itemModel = itemModel;
    }
    
    @Required
    @Autowired
	public void setBeanValidator(Validator beanValidator) {
		this.beanValidator = beanValidator;
	}

	public Validator getBeanValidator() {
		return beanValidator;
	}
    
}

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

Success and Failure JSP’s

The following file will be called when the data is accepted by the Controller class. When validation fails the JSP will display the errors. Notice we are using the customized tag classes defined above and the commandName and name attributes are required. This means that the error page would need to be customized for each form submission. If you guys are able find a way to make this generic then please send me a comment below.

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

<%@ taglib prefix="jserror"	uri="http://test.com/jsrestlib" %>

<jserror:form commandName="item" name="item" method="post">
	<jserror:jserrors path="*"/>
</jserror:form>

Validation configuration

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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE form-validation PUBLIC
          "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN"
          "http://jakarta.apache.org/commons/dtds/validator_1_1_3.dtd">
 
<form-validation>
<formset>
<form name="item">

    <field property="name" depends="required">
        <arg key="item.name" />
    </field>
    <field property="description" depends="required,minlength,maxlength">
        <arg0 key="item.description" />
        <arg1 name="minlength" key="${var:minlength}" resource="false"/>
        <arg1 name="maxlength" key="${var:maxlength}" resource="false"/>
        <var>
            <var-name>minlength</var-name>
            <var-value>5</var-value>
        </var>
        <var>
            <var-name>maxlength</var-name>
            <var-value>10</var-value>
        </var> 
    </field>
        <field property="stockQty" depends="integer">
        <arg0 key="item.stockQty" />
    </field>
    <field property="color" depends="required,mask">
        <msg name="mask" key="item.color.must.be"/>
        <arg0 key="item.color" />
        <var><var-name>mask</var-name><var-value>blue</var-value></var>
    </field>
</form>
</formset>
</form-validation>

src/main/webapp/WEB-INF/validator-rules.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE form-validation PUBLIC
          "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1.3//EN"
          "http://jakarta.apache.org/commons/dtds/validator_1_1_3.dtd">
 
<form-validation>
    <global>
        <validator name="required"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateRequired"
            methodParams="java.lang.Object,
                        org.apache.commons.validator.ValidatorAction,
                        org.apache.commons.validator.Field,
                        org.springframework.validation.Errors"
            msg="errors.required">
        </validator>
        <validator name="requiredif"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateRequiredIf"
            methodParams="java.lang.Object,
                               org.springframework.validation.ErrorsAction,
                               org.apache.commons.validator.Field,
                               org.springframework.validation.Errors"
            msg="errors.required" />
 
        <validator name="validwhen" msg="errors.required"
            classname="org.apache.struts.validator.validwhen.ValidWhen" method="validateValidWhen"
            methodParams="java.lang.Object,
                       org.springframework.validation.ErrorsAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors" />
 
        <validator name="minlength"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateMinLength"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.minlength"
            jsFunction="org.apache.commons.validator.javascript.validateMinLength" />
 
        <validator name="maxlength"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateMaxLength"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.maxlength"
            jsFunction="org.apache.commons.validator.javascript.validateMaxLength" />
 
        <validator name="mask"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateMask"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.invalid" />
 
        <validator name="byte"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateByte"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.byte" jsFunctionName="ByteValidations" />
 
        <validator name="short"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateShort"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.short" jsFunctionName="ShortValidations" />
 
        <validator name="integer"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateInteger"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.integer" jsFunctionName="IntegerValidations" />
 
        <validator name="long"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateLong"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.long" />
 
        <validator name="float"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateFloat"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.float" jsFunctionName="FloatValidations" />
 
        <validator name="double"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateDouble"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.double" />
 
        <validator name="date"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateDate"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.date" jsFunctionName="DateValidations" />
 
        <validator name="intRange"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateIntRange"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="integer" msg="errors.range" />
 
        <validator name="floatRange"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateFloatRange"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="float" msg="errors.range" />
 
        <validator name="doubleRange"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateDoubleRange"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="double" msg="errors.range" />
 
        <validator name="creditCard"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateCreditCard"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.creditcard" />
 
        <validator name="email"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateEmail"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.email" />
 
        <validator name="url"
            classname="org.springmodules.validation.commons.FieldChecks" method="validateUrl"
            methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
            depends="" msg="errors.url" />
    </global>
</form-validation>

Form Panel

The following html page contains the extJS form panel. When the user clicks submit, an Ajax call will be made to the server. If there are any form validation failures they will be displayed next to the text boxes. If all is okay then the page will display a modal success dialog.

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'
            }
        ],     
 
    });
 
    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, 	enableColumnHide: false, 
    	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({})}
];
    

    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: 'itemQty', allowBlank: true},
                  {name: 'color', allowBlank: true}
        ]
    });
     
    // 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>

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>
</body>
</html>

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/

An extJS form should display allowing you to enter values. Click submit and an Ajax form submission will be made to the Spring MVC Controller. Any validation failures will show next to the component.

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

Next Steps

That’s all for now.

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!

28
Mar
11

Using Spring to perform CRUD on an HSQLDB using Ibatis

The following page describes how to create a simple web application that performs CRUD operations against the database using Ibatis SQL Mapper and the Spring Framework and an in-memory Java database called HyperSQL. Since we are using HSQLDB there is no need to set-up an external database. This page contains all the code you need to have a working database driven application.

Note: This page was originally written on Nov 6 2009.

Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

Background

CRUD = Create Read Update Delete.

The application described here presents the user with a simple html form that allows them to create read update and delete items in the database. The Spring JDBC template and Ibatis is used to execute SQL statements.

Requirements

  1. Maven 2
  2. Java 6
  3. Servlet 2.5
  4. Ibatis 2
  5. Spring 2.5
  6. Basic understanding of Spring and Ibatis

Procedure

Outline

  1. Create a new Project
  2. Database Setup and Configuration
  3. Spring Application
  4. Integration with Ibatis
  5. Tying it all Together with Spring MVC
  6. Web Application Configuration
  7. Test the application

Create a new project

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

groupId: com.test
artifactId: crud

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

cd to the project base folder.

cd crud

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.

vi 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>crud</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>crud Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <pluginRepositories>
      <pluginRepository>
        <id>numberformat-releases</id>
        <url>https://raw.github.com/numberformat/20130213/master/repo</url>
      </pluginRepository>
    </pluginRepositories>

    <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>crud</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>/crud</contextPath>
                    </webAppConfig>
                    <stopKey>s</stopKey>
                </configuration>
            </plugin>
            <plugin>
                <groupId>github.numberformat</groupId>
                <artifactId>blog-plugin</artifactId>
                <version>1.0-SNAPSHOT</version>
                <configuration>
                <gitUrl>https://github.com/numberformat/20110328</gitUrl>
                </configuration>
            <executions>
              <execution>
                <id>1</id>
                <phase>site</phase>
                <goals>
                  <goal>generate</goal>
                </goals>
              </execution>
            </executions>
            </plugin>
        </plugins>

    </build>
</project>

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.

vi src/main/resources/ddl.xml

<?xml version="1.0"?>
<!DOCTYPE database SYSTEM "http://db.apache.org/torque/dtd/database.dtd">

<database name="testdb">

    <table name="item">
        <column name="item_id" type="INTEGER" required="true"
            primaryKey="true" autoIncrement="true" />
        <column name="category_id" type="INTEGER" />
        <column name="oem_id" type="VARCHAR" size="50"/>
        <column name="item_x" type="VARCHAR" size="100" />
        <column name="special_f" type="CHAR" size="1" />
        <column name="description" type="VARCHAR" size="1000" />
        <column name="color" type="VARCHAR" size="20" />
        <column name="image_url" type="VARCHAR" size="100" />
        <column name="price" type="DECIMAL" size="15" />
        <column name="length" type="DECIMAL" size="10" />
        <column name="width" type="DECIMAL" size="10" />
        <column name="height" type="DECIMAL" size="10" />
        <column name="weight" type="DECIMAL" size="10" />
        <column name="stock_qty" type="DECIMAL" size="10" />
        <index name="item_item_x">
            <index-column name="item_x" />
        </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.

vi 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

  • Item – POJO that represents the individual item
  • ItemModel – Service Interface
  • ItemModelImpl – Implementation of the Service
  • ItemDataManager – DataManager Interface
  • ItemDataManagerImpl – 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“.

Item Bean

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

package com.test;

import java.math.BigDecimal;

/**
 * Represents an item in inventory or in a shopping cart.
 */
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 BigDecimal length;
    private BigDecimal width;
    private BigDecimal height;

    /**
     * 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 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;
    }
    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;
    }

}

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.

vi src/main/java/com/test/ItemModel.java

package com.test;

import java.util.List;

public interface ItemModel {
    /**
     * This is a pass-thru method that just calls the DataManager.
     */
    public Item getItemInfo(Integer itemId);

    public Item createItem(Item item);

    public void updateItem(Item item);

    public void deleteItem(Integer itemId);

    /**
     * This is a pass-thru method that just calls the DataManager.
     */
    public List<Item> getAllItems(int skipResults, int maxItems);

    /**
     * This is a pass-thru method that just calls the DataManager.
     */
    public int getItemCount();

    public void deleteItemImage(Integer itemId);

}

The following is the implementation class.

vi src/main/java/com/test/ItemModelImpl.java

package com.test;

import java.util.List;

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

@Service
public class ItemModelImpl implements ItemModel {

    @Autowired
    @Qualifier("simpleItemDataManager")
    private ItemDataManager itemDataManager;   

    @Override
    public Item createItem(Item item) {
        return itemDataManager.createItem(item);
    }

    @Override
    public void deleteItem(Integer itemId) {
        itemDataManager.deleteItem(itemId);
    }

    @Override
    public void deleteItemImage(Integer itemId) {
        itemDataManager.deleteItemImage(itemId);
    }

    @Override
    public List<Item> getAllItems(int skipResults, int maxItems) {
        return itemDataManager.getAllItems(skipResults, maxItems);
    }

    @Override
    public int getItemCount() {
        return itemDataManager.getItemCount();
    }

    @Override
    public Item getItemInfo(Integer itemId) {
        return itemDataManager.getItemInfo(itemId);
    }

    @Override
    public void updateItem(Item item) {
        itemDataManager.updateItem(item);
    }

    public void setItemDataManager(ItemDataManager itemDataManager) {
        this.itemDataManager = itemDataManager;
    }

    public ItemDataManager getItemDataManager() {
        return itemDataManager;
    }

}

Spring Data Managers

Data Managers isolate the Model from the implementation of the data store.

vi src/main/java/com/test/ItemDataManager.java

package com.test;

import java.util.List;

public interface ItemDataManager {

    public Item getItemInfo(Integer itemId);

    public Item createItem(Item item);

    public void updateItem(Item item);

    public void deleteItem(Integer itemId);

    List<Item> getAllItems(int skipResults, int maxItems);

    int getItemCount();

    public void deleteItemImage(Integer itemId);
}

The following is the data manager implementation class.

vi src/main/java/com/test/ItemDataManagerImpl.java

package com.test;

import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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;

/**
 * This is the non-ibatis implementation.
 */
@Repository("itemDataManager")
public class ItemDataManagerImpl implements ItemDataManager {
    private final Log logger = LogFactory.getLog(ItemDataManagerImpl.class);
    private SqlMapClientTemplate sqlMapClientTemplate;

    public SqlMapClientTemplate getSqlMapClientTemplate() {
        return sqlMapClientTemplate;
    }

    @Autowired
    @Required
    public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) {
        this.sqlMapClientTemplate = sqlMapClientTemplate;
    }

    @Override
    public Item getItemInfo(Integer itemId) {
        return (Item)getSqlMapClientTemplate().queryForObject("getItemInfo", itemId);
    }

    @Override
    public Item createItem(Item item) {
        Integer id = (Integer) getSqlMapClientTemplate().insert("itemInsert", item);
        item.setItemId(id);
        return item;
    }

    @Override
    public void deleteItem(Integer itemId) {
        if(itemId != null) {
            getSqlMapClientTemplate().delete("deleteItem", itemId);
        }
    }
    @Override
    public void deleteItemImage(Integer itemId) {
        if(itemId != null) {
            getSqlMapClientTemplate().delete("deleteItemImage", itemId);
        }
    }

    @Override
    public void updateItem(Item item) {
        getSqlMapClientTemplate().update("updateItem", item);
    }

    @Override
    public int getItemCount() {
        return 0;
    }

    @Override
    public List<Item> getAllItems(int skipResults, int maxItems) {
        List<Item> list = null;

        list = getSqlMapClientTemplate().queryForList("allItems", null, skipResults, maxItems);

        return list;
    }

}

Spring Configuration

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

vi 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>

Configure simple logging to the console.

vi src/main/resources/log4j.properties

# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=INFO, A1

log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d %-5p %c - %m%n

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

vi 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/ItemDataManager.xml"/>
</sqlMapConfig>

vi 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" />

    <select id="allItems" resultClass="Item">
        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" resultClass="Item">
        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#, #length#, #width#,
            #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 = #length#,
            width = #width#, height = #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>

Tying it all Together with Spring MVC

The following is the controller class that handles the http form submissions.

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

package com.test;

import java.util.Date;
import java.util.List;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/item")
public class ItemController {

    private ItemModel itemModel;

    @RequestMapping(method = RequestMethod.GET)
    public String itemInfo(HttpServletRequest request) {
        List <Item> items = itemModel.getAllItems(0, 100);
        request.setAttribute("items", items);
        return "index";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String createItem(HttpServletRequest request,
            HttpServletResponse response) {

        if ("C".equals(request.getParameter("operation"))) {
            System.out.println("create");
            Item item = new Item();
            item.setName("item created: " + new Date());
            itemModel.createItem(item);
            String message = "newly created item: " + item.getItemId();
            System.out.println(message);
            request.setAttribute("message", message);

        } else if ("R".equals(request.getParameter("operation"))) {
            // left for the reader to implement
            Integer id = new Integer(request.getParameter("id"));

        	System.out.println(itemModel.getItemInfo(id));
        } else if ("U".equals(request.getParameter("operation"))) {
            // left for the reader to implement
            Integer id = new Integer(request.getParameter("id"));
        	Item item = itemModel.getItemInfo(id);
        	item.setName("item updated: " + new Date());
        	itemModel.updateItem(item);
        } else if ("D".equals(request.getParameter("operation"))) {
            Integer id = new Integer(request.getParameter("id"));
            itemModel.deleteItem(id);
            String message = "item deleted: " + id;
            System.out.println(message);
            request.setAttribute("message", message);
        }

        request.setAttribute("items", itemModel.getAllItems(0, 100));
        return "index";
    }

    public ItemModel getItemModel() {
        return itemModel;
    }

    @Required
    @Autowired
    public void setItemModel(ItemModel itemModel) {
        this.itemModel = itemModel;
    }

}

Create the Test HTML Form

The following form issues a POST request to the spring MVC controller. The form is missing the “action” attribute because by default the form posts back to the originating URL. In this case the originating URL is “/apps/item”

vi 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>
<title>DB Parameters</title>
<meta http-equiv="Content-type" content="text/html; charset=iso-8859-1" />
<meta http-equiv="Content-Language" content="en-us" />

</head>
<body>
<h3><c:out  value="${message}"/></h3>

<h3>Items:</h3>
<table border="1">
<thead>
    <td>Id</td>
    <td>Name</td>
</thead>
<tbody>
<c:forEach items="${items}" var="item" >
    <tr>
    <td><c:out value="${item.itemId}"/></td>
    <td><c:out value="${item.name}"/></td>
    </tr>
</c:forEach>
</tbody>
</table>

    <form method="post">
        <label>Operation: </label>
        <select name="operation">
            <option value="C">Create</option>
            <option value="R">Read</option>
            <option value="U">Update</option>
            <option value="D">Delete</option>
        </select>
        <br/>
        <label>ID: </label><input type="text" name="id"/><br/>
        <input type="submit" value="submit"/><br/>
    </form>

This page saves data to an HyperSQL database located in the src/main/webapp/WEB-INF/db folder.
</body>
</html>

Web Application Configuration

The ordering of the listeners are important. You want to make sure that the spring application context is initialized before the DBLifecycleListener.

vi 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-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>

vi src/main/webapp/WEB-INF/spring-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<?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>

Start Jetty Servlet Engine

mvn clean compile jetty:run

Test the application

 

 

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 following location…

target/crud-1.0-SNAPSHOT.war

Next Steps

Full downloadable source for this page is available here.
25
Feb
11

MySQL Test Connection using Spring MVC

This page will describe how to create a spring MVC application to test a connection to MySQL database. The user will provide the connection URL and username/password. The system will make a simple connection to the database and return the results to the screen.

Note: This page was originally written in November 15 2009.

Requirements

  • Access to a MySQL Server
  • Maven 2
  • Eclipse
  • Spring 2.5
  • Java 1.5 or above

Start a new Project

Start a new project by creating it from an archetype.

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

Answer the questions like this:

Define value for groupId: : test
Define value for artifactId: : spring-mysql-test
Define value for version:  1.0-SNAPSHOT: :
Define value for package:  test: :
Confirm properties configuration:
groupId: test
artifactId: spring-mysql-test
version: 1.0-SNAPSHOT
package: test
 Y: :

cd spring-mysql-test
modify pom.xml and insert the following dependencies

	<dependencies>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.9</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>org.apache.geronimo.specs</groupId>
		<artifactId>geronimo-servlet_2.5_spec</artifactId>
		<version>1.2</version>
		<type>jar</type>
		<scope>provided</scope>
	</dependency>
	</dependencies>

insert the following into

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

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <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>

<welcome-file-list>
    <welcome-file>/app/helloWorld</welcome-file>
</welcome-file-list>

</web-app>

Insert the following into

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:p="http://www.springframework.org/schema/p" 
    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:component-scan base-package="test" />

	<bean id="viewResolver"
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/WEB-INF/jsp/" />
		<property name="suffix" value=".jsp" />
	</bean>

</beans>

Java Code

Before writing the Java files please create the following directory.

mkdir -p src/main/java/test

Create a java class that looks like this…
src/main/java/test/HelloWorldController.java

package test;

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

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import org.springframework.stereotype.Controller;

@Controller("/helloWorld")
public class HelloWorldController extends AbstractController {
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest request,
			HttpServletResponse response) throws Exception {

		ModelAndView mav = null;
		// control logic goes here
		mav = new ModelAndView("helloWorld");
		return mav;
	}
}

mkdir -p src/main/webapp/WEB-INF/jsp

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

<!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>
		<title>HelloWorld test page</title>
		<meta http-equiv="Content-type" content="text/html; charset=iso-8859-1" />
		<meta http-equiv="Content-Language" content="en-us" />
	</head>
	
	<body>
		Hello World page!
	</body>
</html>

Run the Project

In order to run the project we need to put a couple of more things into pom.xml file.

1. The jetty plugin will allow Maven to run the project in the jetty servlet container. In the plug-in section of the pom.xml file put in the following…
2. By default Maven uses an old version of the JDK to compile. We need to set it to use a higher version. (supports annotations). If you don’t have 1.6 then change the source and target elements below to 1.5 at least.

pom.xml inside the build tag

  <build>
    <finalName>spring-mysql-test</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>1</scanIntervalSeconds>
				</configuration>
			</plugin>
		</plugins>
  </build>

To run the web application type:
type

mvn jetty:run

Navigate your browser to http://localhost:8080/app/helloWorld to test if everything is okay up to this point. If something is not working please review the steps seen above and make sure you at least see hello world printed on the screen.

We will create a Controller that draws a input form that the user can use to specify database url, username, password.

src/main/java/test/DBParam.java

package test;

public class DBParam {
    public String url;
    public String username;
    public String password;
    
	public String getUrl() {
		return url;
	}
	public void setUrl(String url) {
		this.url = url;
	}
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}

}

The controller will take this form submission and make a connection to the database with the results printed to the screen.

src/main/java/test/TestDBConnectionController.java

package test;

import java.sql.Connection;
import java.sql.DriverManager;

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

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;

@Controller("/dbParameters")
public class TestDBConnectionController extends SimpleFormController {

	public TestDBConnectionController() {
		setFormView("dbParameters");
		setCommandClass(DBParam.class);
		setSuccessView("success");
		setCommandName("dbParam");
	}

	@Override
	protected ModelAndView processFormSubmission(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors)
			throws Exception {
		System.out.println("form submitted");

		DBParam dbParam = (DBParam)command;
		String url = dbParam.getUrl();
		String username = dbParam.getUsername();
		String password = dbParam.getPassword();

		// make a connection
		Class.forName("com.mysql.jdbc.Driver").newInstance();
		Connection conn = DriverManager.getConnection(url, username, password);
		System.out.println("got a connection: " + conn);

		return super.processFormSubmission(request, response, command, errors);
	}
}

We have enabled component scanning in our spring-servlet.xml file so we don’t have to register this controller in the xml file.

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

<!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>
<title>DB Parameters</title>
<meta http-equiv="Content-type" content="text/html; charset=iso-8859-1" />
<meta http-equiv="Content-Language" content="en-us" />

</head>
<body>
	<form method="post">
		<label>JDBC connect string: </label><input type="text" name="url" value="jdbc:mysql://hostname:3306/dbname" size="55"/><br/>
		<label>password: </label><input type="text" name="username"/><br/>
		<label>username: </label><input type="password" name="password"/><br/>
		<input type="submit" value="submit"/><br/>
	</form>
</body>
</html>

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

<!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>
<title>DB Parameters</title>
<meta http-equiv="Content-type" content="text/html; charset=iso-8859-1" />
<meta http-equiv="Content-Language" content="en-us" />

</head>
<body>
	success
</body>
</html>

Run the project

You may run the project under jetty

mvn jetty:run

navigate your browser to:

http://localhost:8080/app/dbParameters

Replace the URL with the url to your mysql database.

That’s it for now!!!




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

Join 75 other followers

April 2017
S M T W T F S
« Mar    
 1
2345678
9101112131415
16171819202122
23242526272829
30  

Blog Stats

  • 807,309 hits