This page demonstrates the process of using Spring Batch and the Apache Commons Validator Framework to validate each record in a flat file. To keep this tutorial brief we will stop processing the file and print out the validation failure message on the first occurrence.
Background
The Commons Validator Framework is composed of a core set of classes responsible for validating data. Since the framework is versatile and does not make assumptions about implementation, you can’t use it out of the box unless you write wrapper code to adapt it to your needs.
Years ago the popular Struts framework had provided a set of wrapper classes in the “org.apache.struts.validator” package. The spring MVC framework had made similar adaptations. I had found it strange that the makers of spring batch did not have a drop in replacement so that had lead me to write this page.
Requirements
This tutorial continues from where the previous one left of so please complete the examples in those articles first before proceeding.
Procedure
Start by adding the following dependencies into your pom.xml file.
pom.xml
<!-- The commons Validator Framework -->
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.1.4</version>
</dependency>
<!-- Spring Wrapper classes for the Validator Framework -->
<dependency>
<groupId>org.springmodules</groupId>
<artifactId>spring-modules-validation</artifactId>
<version>0.8</version>
</dependency>
The following is the core class that makes this all happen.
src/main/java/com/test/SpringCommonsValidator.java
package com.test;
import java.util.List;
import org.springframework.batch.item.validator.ValidationException;
import org.springframework.batch.item.validator.Validator;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
/**
* Allows for the commons validation framework to print human readable
* validation failure messages. Also its very good at adapting the
* Spring Validator Interface to Spring Batch Validator, thus allowing
* this class to be used as a Validator in the spring batch framework.
*
* This class delegates most of its work to the spring validator.
*
*/
public class SpringCommonsValidator<T> implements Validator<T> {
private org.springframework.validation.Validator validator;
private MessageSource messageSource;
public void validate(T item) throws ValidationException {
if (!validator.supports(item.getClass())) {
throw new ValidationException("Validation failed for " + item + ": " + item.getClass().getName()
+ " class is not supported by validator.");
}
BeanPropertyBindingResult errors = new BeanPropertyBindingResult(item, "item");
validator.validate(item, errors);
if (errors.hasErrors()) {
throw new ValidationException("Validation failed for " + item + ": " + errorsToString(errors));
}
}
private String errorsToString(Errors errors) {
@SuppressWarnings("rawtypes")
List errorList = errors.getAllErrors();
StringBuffer buffer = new StringBuffer();
boolean first = true;
for (Object object : errorList) {
if(object instanceof MessageSourceResolvable) {
MessageSourceResolvable r = (MessageSourceResolvable)object;
if(!first) { buffer.append("; "); }
buffer.append(messageSource.getMessage(r, null));
first = false;
}
}
return buffer.toString();
}
public void setValidator(org.springframework.validation.Validator validator) {
this.validator = validator;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
public MessageSource getMessageSource() {
return messageSource;
}
}
Resource Bundle
The following property file is used to display the error messages. I guess some additional work can be done to internationalize these messages but since batch programs are not end-client facing… what’s the point?
On the other hand, having error messages in a property files does have its advantages.
src/main/resources/messages.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.
Configuration
Modify the spring configuration file to add the validatorFactory, beanValidator and springValidator. These classes allow you to have the ValidatingItemProcessor validate each item that is passed to it.
src/main/resources/simpleJob.xml
Replace the following section
<!-- Processor -->
<beans:bean name="empProcessor" class="com.test.EmployeeProcessor">
</beans:bean>
with the following
<!-- Processor -->
<beans:bean name="empProcessor" class="org.springframework.batch.item.validator.ValidatingItemProcessor">
<beans:property name="validator" ref="springValidator"/>
</beans:bean>
<!-- Validator -->
<beans:bean id="validatorFactory"
class="org.springmodules.validation.commons.DefaultValidatorFactory">
<beans:property name="validationConfigLocations">
<beans:list>
<beans:value>validation.xml</beans:value>
<beans:value>validator-rules.xml</beans:value>
</beans:list>
</beans:property>
</beans:bean>
<beans:bean id="beanValidator" class="org.springmodules.validation.commons.DefaultBeanValidator">
<beans:property name="validatorFactory" ref="validatorFactory"/>
<beans:property name="useFullyQualifiedClassName" value="true"/>
</beans:bean>
<beans:bean id="springValidator" class="com.test.SpringCommonsValidator">
<beans:property name="validator" ref="beanValidator"/>
<beans:property name="messageSource" ref="resourceBundleMessageSource"/>
</beans:bean>
<beans:bean id="resourceBundleMessageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<beans:property name="basename" value="messages"/>
</beans:bean>
Commons validator configuration
The following is the validation configuration file used by commons-validator. If you have used the struts framework before then this file may look familiar to you. This file contains some default “shrink-wrapped” validation checks that were originally included with struts. There is nothing to customize here for now so please just copy and paste this into your program and continue on.
src/main/resources/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>
Validation Rules
The next file contains the actual validations that need to be performed.
These validations are:
- last Name is required
- last Name must be between 20 and 35 characters in length
- rank is required.
src/main/resources/validation.xml
<!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="com.test.Employee">
<field property="lastName" depends="required,minlength,maxlength">
<arg0 key="lastName" />
<arg1 name="minlength" key="${var:minlength}" resource="false"/>
<arg1 name="maxlength" key="${var:maxlength}" resource="false"/>
<var>
<var-name>minlength</var-name>
<var-value>20</var-value>
</var>
<var>
<var-name>maxlength</var-name>
<var-value>35</var-value>
</var>
</field>
<field property="rank" depends="required">
<arg key="rank" />
</field>
</form>
</formset>
</form-validation>
Run the batch
Run the batch application by typing the following on the command line.
mvn clean compile exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"
Observe the Results
The following message should display on the console indicating a validation failure.
SEVERE: Encountered an error executing the step
org.springframework.batch.item.validator.ValidationException: Validation failed for Employee [empId=7876, lastName=ADAMS, title=CLERK, salary=1100, rank=null]: lastName can not be less than 20 characters.; rank is required.
at com.test.SpringCommonsValidator.validate(SpringCommonsValidator.java:38)
at org.springframework.batch.item.validator.ValidatingItemProcessor.process(ValidatingItemProcessor.java:77)
at org.springframework.batch.core.step.item.SimpleChunkProcessor.doProcess(SimpleChunkProcessor.java:125)
at org.springframework.batch.core.step.item.SimpleChunkProcessor.transform(SimpleChunkProcessor.java:288)
Next Steps
The application described here is a quick and dirty approach to illustrate batch validation. In a typical production grade application you will want to allow the job to continue processing the remaining records in a file and send the bad records to a error file. The error file can be reviewed manually and re-processed on the next run.
Comments and feedback are Welcome.
Thanks!