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.

Advertisements

Leave a Reply

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

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s


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

Join 77 other followers

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

Blog Stats

  • 846,577 hits

%d bloggers like this: