Posts Tagged ‘spring

31
May
13

How to initialize Spring Framework inside JAX-WS Service

This page describes how to write a JAX-WS, Spring Framework based web service.

JAX-WS makes it easier to write web services in Java. It shilds the programmer from implementation
specifics just like JDBC API did for databases.

The two popular implementations out there are the Apache CXF and the Java Reference Implementation. This page describes an application that will run on both.

The project will be tested in Tomcat 6 with the Reference Implementation and we will move to JBoss that has a built in CXF implementation.

Few Notes about the application servers

  • Tomcat 6 does not ship with JAX-WS RI (needs to be installed) or the Spring Framework (needs to be included in the WAR)
  • JBoss 5.1 has the CXF and Spring Framework built in to the server. Therefore don’t provide your own JAX-WS or spring implementation JARS.
  • The Spring application context should be initialized first. Use the ContextLoaderListener to accomplish this.
  • The JAX-WS endpoint servlet/class should be defined in the web.xml and annotated with the JAX-WS annotations. In addition you should use the @PostConstruct on one of the methods to initialize. Within the method use the SpringBeanAutowiringSupport. See the Spring user manual for an example code.

Software versions

  • Tomcat 6 and/or JBoss 5.1
  • w. Servlet Spec 2.5
  • Java 6
  • Spring 2.5.6.SEC01 – older version used since JBOSSWS 5.1 comes pre-packaged with it.
  • Maven 3
  • Soap UI eclipse plugin – to call the web service

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/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>jax-ws-spring-jboss</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
	<dependencies>
	<dependency>
		<artifactId>spring-context</artifactId>
		<groupId>org.springframework</groupId>
		<version>2.5.6.SEC01</version>
	</dependency>
	<dependency>
		<artifactId>spring-core</artifactId>
		<groupId>org.springframework</groupId>
		<version>2.5.6.SEC01</version>
	</dependency>
	<dependency>
		<artifactId>spring-beans</artifactId>
		<groupId>org.springframework</groupId>
		<version>2.5.6.SEC01</version>
	</dependency>

	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-web</artifactId>
		<version>2.5.6.SEC01</version>
	</dependency>

	</dependencies>
	<build>
		<finalName>jax-ws-spring-jboss</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
		</plugins>
	</build>  
</project>

src/main/java/com/test/Customer.java

package com.test;

import java.io.Serializable;

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

@XmlRootElement(name="customer",namespace="http://com/test/")
public class Customer implements Serializable {
	private static final long serialVersionUID = 1L;

	private String id;
    private String name;
     
    public Customer() {
		super();
	}
	public Customer(String id) {
		super();
		this.id = id;
	}
	public Customer(String id, String name) {
		super();
		this.id = id;
		this.name = name;
	}
	@XmlElement(nillable=false,namespace="http://com/test/")
	public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
	@XmlElement(nillable=false,namespace="http://com/test/")
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

src/main/java/com/test/CustomerService.java

package com.test;

import java.util.List;

import javax.jws.WebMethod;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
import javax.jws.soap.SOAPBinding.Style;

@WebService(targetNamespace="http://com/test/")
@SOAPBinding(style=Style.DOCUMENT)
public interface CustomerService {
    @WebMethod @WebResult(name="customer")
    List<Customer> getCustomer(String customerID);
}

src/main/java/com/test/CustomerServiceImpl.java

package com.test;

import java.util.List;

import javax.annotation.PostConstruct;
import javax.jws.WebService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;

@WebService(endpointInterface="com.test.CustomerService") 
public class CustomerServiceImpl implements CustomerService {

	@Autowired
	private TestDataManager testDataManager;
	
	public TestDataManager getTestDataManager() {
		return testDataManager;
	}
	public void setTestDataManager(TestDataManager testDataManager) {
		this.testDataManager = testDataManager;
	}
	@PostConstruct
	public void postConstruct() {
		System.out.println("postconstruct has run.");
		SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
	}
	@Override
    public List<Customer> getCustomer(String customerId) {
    	System.out.println("JAX-WS getCustomer called");
		return testDataManager.getCustomers();
    }
}

src/main/java/com/test/TestDataManager.java

package com.test;

import java.util.ArrayList;
import java.util.List;

public class TestDataManager {
	/* (non-Javadoc)
	 * @see com.test.TestDataManager#getCustomers()
	 */
	public List<Customer> getCustomers() {
		List<Customer> customerList = new ArrayList<Customer>();
		customerList.add(new Customer("1"));
		customerList.add(new Customer("2"));
		return customerList;
	}
	
}

src/main/webapp/WEB-INF/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">

  <bean id="testDataManager" class="com.test.TestDataManager"/>

</beans>

src/main/webapp/WEB-INF/sun-jaxws.xml

<?xml version="1.0" encoding="UTF-8"?>
<endpoints
  xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime"
  version="2.0">
  <endpoint
      name="CustomerService"
      implementation="com.test.CustomerServiceImpl"
      url-pattern="/customerService"/>
</endpoints>

Running within JBossWS 5.1 container

As mentioned above the JBossWS continer comes prepackaged with Spring Framework 2.5.6.SEC01. To make things easier I have used this older version has been used for Tomcat 6 as well. But interoperatibility is not a problem between Tomcat 6 and JBoss then feel free to use a newer version of the framework.

Since JBoss comes with the Spring Framework and by default it is packaged into the WAR we will need to tell JBoss to use the “parent-first” class loading scheme.

This is done by creating the following file into the WEB-INF folder of the WAR.

src/main/webapp/WEB-INF/jboss-classloading.xml

<classloading xmlns="urn:jboss:classloading:1.0"
    domain="yourDomain"
    parent-first="true">
</classloading>

JBoss uses a different method to initialize the JAX-WS Endpoints. It expects the endpoints to be defined in web.xml as regular servlets. This eliminates the need for the sun-jaxws.xml file.

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

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<!-- Initialize the spring framework first. -->
    <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>	

	<!-- Use the following with JBossWS CXF implementation 
	<servlet>
		<servlet-name>customerService</servlet-name>
		<servlet-class>com.test.CustomerServiceImpl</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>customerService</servlet-name>
		<url-pattern>/customerService</url-pattern>
	</servlet-mapping>
	-->

	<!-- Use the following with Tomcat and JAX-WS RI 
	-->
    <listener>
        <listener-class>
                com.sun.xml.ws.transport.http.servlet.WSServletContextListener
        </listener-class>
    </listener>
    <servlet>
        <servlet-name>customerService</servlet-name>
        <servlet-class>
            com.sun.xml.ws.transport.http.servlet.WSServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>customerService</servlet-name>
        <url-pattern>/customerService</url-pattern>
    </servlet-mapping>

	<!-- Test the application using the following URL: 
	http://localhost:8080/jax-ws-spring-jboss/customerService?wsdl -->

</web-app>

The web.xml file above contains both versions of the initilization logic. Comment out the proper section to run the application in JBoss or Tomcat. This is the only file you will need to change when switching between application servers.

Test using the SOAP UI plugin

Soap UI is the best interface I have seen so far to develop test and debug web services. Use it to submit a request to the server and you will see the following response.

Request

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:test="http://com/test/">
   <soapenv:Header/>
   <soapenv:Body>
      <test:getCustomer>
         <!--Optional:-->
         <arg0>1</arg0>
      </test:getCustomer>
   </soapenv:Body>
</soapenv:Envelope>

Response

<?xml version="1.0" encoding="UTF-8"?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Body>
      <ns2:getCustomerResponse xmlns:ns2="http://com/test/">
         <customer>
            <ns2:id>1</ns2:id>
         </customer>
         <customer>
            <ns2:id>2</ns2:id>
         </customer>
      </ns2:getCustomerResponse>
   </S:Body>
</S:Envelope>
09
Feb
12

ALTERing Database Schema Using DDLUtils

This page describes how to use DDLUtils to only prints out alter statements instead of making the changes automatically.

Requirements

Background

My previous article described how to use DDLUtils to automatically make changes to a database. Since we were using an in-memory/temporary database, it wouldn’t be a big deal if DDLUtils made a mistake.

Shortcut: If you have not implemented the “In memory database” in the requirements section and you have your own database. Then you can skip to the short cut section below. the return here.

DDLUtils has functionality to fetch the current DDL and dump it into an XML file. Once we modify the xml we can have DDLUtils compare the xml against the current database and only print “ALTER” statements to the screen. We can take those alter statements and manually run them against the database.

Create the Java Code

The following class is designed to be run from the command line. You can wrap it into a shell script. Take some time to review the java source below. It is pretty much self explanatory. Please be sure to name your xml file different from the prior project. In the example below its called “ddl-draft.xml”.

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

package com.test;

import java.io.InputStreamReader;
import java.io.StringWriter;

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.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * <p>
 * Dumps or generates schema information for the batch project. This class is
 * only designed to output information. Although the framework is capabile, It
 * should not make any alterations to the database since automated alterations
 * could be risky.
 * </p>
 * 
 * <p>
 * The Dump command takes the existing database schema and creates an xml file.
 * You can choose to redirect this output to a file so you can modify the
 * schema.
 * </p>
 * 
 * <p>
 * The alter command takes the xml located in the /ddl.xml file and generates
 * alter statements to standard output. You can redirect this to a file and use
 * it to alter the database manually.
 * </p>
 * 
 * <p>
 * mvn -q exec:java -Dexec.mainClass=com.test.DDLUtils -Dexec.args="dump"
 * </p> language
 * <p>
 * mvn -q exec:java -Dexec.mainClass=com.test.DDLUtils -Dexec.args="alter"
 * </p>
 * 
 */
public class DDLUtils {
	private DataSource dataSource;

	public DDLUtils() {
		super();
		ApplicationContext context = new 
		ClassPathXmlApplicationContext("com/test/app-config.xml");
		dataSource = (DataSource) context.getBean("dataSource");		
	}
	
	public static void main(String args[]) {
		(new DDLUtils()).contextInitialized(args);
	}
	
    public void contextInitialized(String args[]) {
        Platform platform = PlatformFactory
                .createNewPlatformInstance(dataSource);
 
        Database database = new DatabaseIO().read(new InputStreamReader(
                getClass().getResourceAsStream("/ddl-draft.xml")));
        if(args.length < 1) {
        	usage();
        	return;
        }
        if("dump".equals(args[0])) {
    		StringWriter writer = new StringWriter();
    		new DatabaseIO().write(platform.readModelFromDatabase("batch"), writer);
    		System.out.println(writer.toString());        	
        } else if("alter".equals(args[0])) {
        	System.out.println(platform.getAlterTablesSql(database));
        } else {
        	usage();
        	return;
        }
    }

	private void usage() {
    	System.out.println("usage: ");
    	System.out.println("\t\t"+getClass().getName()+" (dump|alter)\n\n");    
	}
    
}

Testing the Application

The following command takes the current schema and creates an xml file to standard output.

mvn -q exec:java -Dexec.mainClass=com.test.DDLUtils -Dexec.args="dump"

The following prints to standard out the DDL that will get the current database to resemble what is documented in the xml file.

mvn -q exec:java -Dexec.mainClass=com.test.DDLUtils -Dexec.args="alter"

Shortcut

Read this section if you already have a database and simply want to use DDL utils to query the schema or help modify it.

Step 1: Create a Maven Project with all the necessary dependencies listed below + the JDBC driver specific to your database.
mkdir ddlutil
cd ddlutil
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>ddlutil</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>ddlutil 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.apache.ddlutils</groupId>
		<artifactId>ddlutils</artifactId>
		<version>1.0</version>    
    </dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring</artifactId>
			<version>2.5.6</version>
		</dependency>                
  </dependencies>
  <build>
    <finalName>ddlutil</finalName>
    
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.0.2</version>
				<configuration>
					<source>1.5</source>
					<target>1.5</target>
				</configuration>
			</plugin>
		</plugins>    
  </build>
</project>

Step 2: Create the spring configuration file with the information necessary to connect to your datasource.

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="xxxreplace with your database driverxxx"/>
        <property name="url" value="xxxreplace with your database urlxxx"/>
        <property name="username" value="xxx_username_xxx"/>
        <property name="password" value="xxx"/>
</bean>
</beans>

See the following URL to setup data connectivity using spring and help fill in the values above.

Step 3: Create sample DDL and return the the Background section on this page.

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="author">
    <column name="author_id"
            type="INTEGER"
            primaryKey="true"
            required="true"/>
    <column name="name"
            type="VARCHAR"
            size="50"
            required="true"/>
    <column name="organization"
            type="VARCHAR"
            size="50"
            required="false"/>
  </table>

  <table name="book">
    <column name="book_id"
            type="INTEGER"
            required="true"
            primaryKey="true"
            autoIncrement="true"/>
    <column name="isbn"
            type="VARCHAR"
            size="15"
            required="true"/>
    <column name="author_id"
            type="INTEGER"
            required="true"/>
    <column name="title"
            type="VARCHAR"
            size="255"
            required="true"/>

    <foreign-key foreignTable="author">
      <reference local="author_id" foreign="author_id"/>
    </foreign-key>  

    <index name="book_isbn">
      <index-column name="isbn"/>
    </index>
  </table>
</database>

References

17
Oct
11

Spring Batch Validation

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:

  1. last Name is required
  2. last Name must be between 20 and 35 characters in length
  3. 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!

07
Oct
11

Restartable Jobs With Spring Batch

This page demonstrates how spring batch handles job failure and how to recover from them.

Background

Please review The Domain Language of Spring Batch prior to reading this further.

StepExecution

In simple terms each “job” is broken up into individual “steps”. The process of executing a step is called a “StepExecution”. Each execution of a step can either result in Success or Failure. If individual steps of a larger job fail it would be nice to know where the step left off. The Designers of this framework recognized this and came up with a way to allow steps to resume. The JobRepository allows Readers and Writers to store status, start and end times, exit codes, and count information for each Execution of a step within a Job. This information is stored using the ExecutionContext at each commit interval.

ExecutionContext

The “ExecutionContext” is a collection of name/value pairs that are persisted to the database. Each instance that a Step is executed by the spring batch framework has its own ExecutionContext. Many of the default Reader/Writer implementations publish information to the ExecutionContext by default.

Outline of this page

Here is a breakdown of what will be demonstrated:

  1. Simulate a job failure by modifying the EmployeeProcessor.java and throw an exception towards the middle of the file.
  2. Examine the output file and verify that all prior records were written out.
  3. Examine the JobRepository database to verify the ExecutionContext record contains the recovery information.
  4. Modify EmployeeProcessor.java to not throw the test exception.
  5. Examine the output file and verify that all the remaining records were written out.
  6. Note that the Footer of the file is incorrect.
  7. Fix the EmployeeItemWriter.java to read the ExecutionContext and output the correct footer.

This page picks up where the last one left of. So if you have not done so already please implement the example application described here.

Requirements

  • Java 5 or above and Maven 2
  • Spring Batch 2.x
  • Successful completion of xxx

Inserting An Error

The below highlighted code stops the process when the record for the “president” is processed.

package com.test;

import org.springframework.batch.item.ItemProcessor;

public class EmployeeProcessor implements ItemProcessor<Employee, Employee> {

	public Employee process(Employee emp) throws Exception {
		if("PRESIDENT".equals(emp.getTitle())) throw new Exception("test exception: found President");
		// if salary >= 2500 then set rank as "Director"		
		if(emp.getSalary() >= 2500 ) {
			emp.setRank("Director");
		} else {
			emp.setRank("N/A");
		}
		return emp;
	}

}

Run the command:

mvn exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"

Open the target/output_data.txt file and observe that only 7 records were processed.

Comment out line 8 from EmployeeProcessor.java above and re-run the batch.

mvn exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"

Open target/output_data.txt one more time and observe that the remaining records were processed however note that the footer information is incorrect. It should say 14 records instead of 7.

Making Spring Batch Restartable

If you remember in the previous article we modified the EmployeeItemWriter class to print out a header and footer records. The footer record was generated using state variables (totalAmount, and recordCount). Since the EmployeeItemWriter maintains state, when the batch job restarts the state variables are re-initialized to their original values. Since the second invocation of the job starts from the middle of the input file the writer does not get an opportunity to properly initialize the state variables.

To fix this problem we will use the executionContext. The executionContext gets updated on each commit interval. It is the place where Spring batch keeps information that is important to the currently running job Execution.

We will modify the open() and update() methods of the EmployeeItemWriter to lookup the values from the ExecutionContext.

/src/main/java/com/test/EmployeeItemWriter.java

	public void open(ExecutionContext executionContext) throws ItemStreamException {
		if(executionContext.containsKey("current.recordCount")) {
			recordCount = new Long(executionContext.getLong("current.recordCount")).intValue();
		} else {
			recordCount = 0;
		}		
		
		if(executionContext.containsKey("current.totalAmount")) {
			totalAmount = new BigDecimal(executionContext.getDouble("current.totalAmount"));
		} else {
			totalAmount = BigDecimal.ZERO;
		}		
		this.delegate.open(executionContext);
	}

	public void update(ExecutionContext executionContext) throws ItemStreamException {
		executionContext.putLong("current.recordCount", recordCount);
		executionContext.putDouble("current.totalAmount", totalAmount.doubleValue());
		
		this.delegate.update(executionContext);
	}

We don’t need to modify “empReader” bean since it is backed by “FlatFileItemReader” and it’s implementation is restartable by default.

Clear the database

In order to start fresh again, Delete all files under src/main/resources/db. This is where HSQLDB keeps its database files. Once you delete this the DDLUtils framework will regenerate the database with all the schema information.

Re-run the job

Comment IN the EmployeeProcessor.java so that when “PRESIDENT” is encountered it throws an exception and re-run the job.

mvn exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"

Comment OUT the line in EmployeeProcessor.java and re-run the job again.

mvn exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"

Verify the output

Open target/output_data.txt one more time and observe that the remaining records were processed and the count should be 14 records.

That’s all for now.

02
Oct
11

Spring Batch HyperSQL Job Repository

This page describes the process of setting up a semi-permanent job repository that will maintain state between job invocations. No external databases will be necessary since HSQLDB is a pure java implementation of a database.

Background

The spring batch framework offers an in-memory Repository that does not persist domain objects once the batch job is complete. This is a severe limitation since:

  • Job restarts will not be possible (resume on restart functionality)
  • Can not guarantee that two job instances with the same parameters are not launched simultaneously
  • Not suitable for multi-threaded job invocations

Requirements

Solution

In order to have the job repository stored in our application we will be using HSQLDB as our database implementation and and DDLUtils to manage the schema.

Modify the pom.xml and insert the following dependencies.

pom.xml

    <dependency>
        <groupId>org.apache.ddlutils</groupId>
        <artifactId>ddlutils</artifactId>
        <version>1.0</version>
    </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>1.8.0.10</version>
        </dependency>

DDL

The following xml file is used by Apache DDLUtils to create the Spring Batch JobRepository database. When the application starts for the first time the tables will be created. Each subsequent invocation will cause the DDLUtils to check the database schema against the xml file. If the database is different then it will make the necessary alters to update the database to reflect the schema represented in the below file.

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="BATCH_JOB_INSTANCE">
		<column name="JOB_INSTANCE_ID" type="BIGINT" primaryKey="true"
			autoIncrement="true"/>
		<column name="VERSION" type="BIGINT" />
		<column name="JOB_NAME" type="VARCHAR" size="100" required="true" />
		<column name="JOB_KEY" type="VARCHAR" size="32" required="true" />
		<unique name="JOB_INST_UN">
		  <unique-column name="JOB_NAME"/>
		  <unique-column name="JOB_KEY"/>
		</unique>
	</table>

    <table name="BATCH_JOB_EXECUTION">
        <column name="JOB_EXECUTION_ID" type="BIGINT" primaryKey="true"/>
        <column name="VERSION" type="BIGINT" />
	    <column name="JOB_INSTANCE_ID" type="BIGINT" required="true"/>
	    <column name="CREATE_TIME" type="TIMESTAMP" required="true"/>
	    <column name="START_TIME" type="TIMESTAMP"/>
	    <column name="END_TIME" type="TIMESTAMP"/>
	    <column name="STATUS" type="VARCHAR" size="10"/>
	    <column name="EXIT_CODE" type="VARCHAR" size="20"/>
	    <column name="EXIT_MESSAGE" type="VARCHAR" size="2500"/>
	    <column name="LAST_UPDATED" type="TIMESTAMP"/>
	    <foreign-key name="JOB_INST_EXEC_FK" foreignTable="BATCH_JOB_INSTANCE">
	       <reference foreign="JOB_INSTANCE_ID" local="JOB_INSTANCE_ID"/>
	    </foreign-key>
    </table>

    <table name="BATCH_JOB_PARAMS">
        <column name="JOB_INSTANCE_ID" type="BIGINT" required="true"/>
	    <column name="TYPE_CD" type="VARCHAR" size="6" required="true"/>
	    <column name="KEY_NAME" type="VARCHAR" size="100" required="true"/> 
	    <column name="STRING_VAL" type="VARCHAR" size="250"/> 
	    <column name="DATE_VAL" type="TIMESTAMP"/> 
	    <column name="LONG_VAL" type="BIGINT"/>
	    <column name="DOUBLE_VAL" type="DOUBLE"/>
        <foreign-key name="JOB_INST_PARAMS_FK" foreignTable="BATCH_JOB_INSTANCE">
           <reference foreign="JOB_INSTANCE_ID" local="JOB_INSTANCE_ID"/>
        </foreign-key>
    </table>

    <table name="BATCH_STEP_EXECUTION">
		<column name="STEP_EXECUTION_ID" type="BIGINT" primaryKey="true" 
		  autoIncrement="true" />
		<column name="VERSION" type="BIGINT" required="true"/>
		<column name="STEP_NAME" type="VARCHAR" size="100" required="true"/>
		<column name="JOB_EXECUTION_ID" type="BIGINT" required="true" />
		<column name="START_TIME" type="TIMESTAMP" required="true"/> 
		<column name="END_TIME" type="TIMESTAMP"/>  
		<column name="STATUS" type=" VARCHAR" size="10"/>
		<column name="COMMIT_COUNT" type="BIGINT"/> 
		<column name="READ_COUNT" type="BIGINT"/>
		<column name="FILTER_COUNT" type="BIGINT"/>
		<column name="WRITE_COUNT" type="BIGINT"/>
		<column name="READ_SKIP_COUNT" type="BIGINT"/>
		<column name="WRITE_SKIP_COUNT" type="BIGINT"/>
		<column name="PROCESS_SKIP_COUNT" type="BIGINT"/>
		<column name="ROLLBACK_COUNT" type="BIGINT"/> 
		<column name="EXIT_CODE" type="VARCHAR" size="20"/>
		<column name="EXIT_MESSAGE" type="VARCHAR" size="2500"/>
		<column name="LAST_UPDATED" type="TIMESTAMP"/>
        <foreign-key name="JOB_EXEC_STEP_FK" foreignTable="BATCH_JOB_EXECUTION">
           <reference foreign="JOB_EXECUTION_ID" local="JOB_EXECUTION_ID"/>
        </foreign-key>
    </table>

    <table name="BATCH_STEP_EXECUTION_CONTEXT">
	    <column name="STEP_EXECUTION_ID" type="BIGINT" primaryKey="true"/>
	    <column name="SHORT_CONTEXT" type="VARCHAR" size="2500" required="true"/>
	    <column name="SERIALIZED_CONTEXT" type="VARCHAR" />
        <foreign-key name="STEP_EXEC_CTX_FK" foreignTable="BATCH_STEP_EXECUTION">
           <reference foreign="STEP_EXECUTION_ID" local="STEP_EXECUTION_ID"/>
        </foreign-key>
    </table>

    <table name="BATCH_JOB_EXECUTION_CONTEXT">
		<column name="JOB_EXECUTION_ID" type="BIGINT" primaryKey="true"/>
		<column name="SHORT_CONTEXT" type="VARCHAR" size="2500" required="true"/>
		<column name="SERIALIZED_CONTEXT" type="VARCHAR"/> 
        <foreign-key name="JOB_EXEC_CTX_FK" foreignTable="BATCH_JOB_EXECUTION">
           <reference foreign="JOB_EXECUTION_ID" local="JOB_EXECUTION_ID"/>
        </foreign-key>        
    </table>
	<table name="BATCH_STEP_EXECUTION_SEQ">
		<column name="ID" type="BIGINT" primaryKey="true" autoIncrement="true" />
	</table>
	<table name="BATCH_JOB_EXECUTION_SEQ">
		<column name="ID" type="BIGINT" primaryKey="true" autoIncrement="true" />
	</table>
	<table name="BATCH_JOB_SEQ">
		<column name="ID" type="BIGINT" primaryKey="true" autoIncrement="true" />
	</table>
    
</database>

Java Code

The following file initializes the Job Repository and creates the schema based on a DDL contained in an xml configuration file. It also allows the system to gracefully shut-down the database and persist the in-memory data back to the file system after batch job completion.

src/main/java/com/test/JobRepositoryInitializer.java

package com.test;

import java.io.InputStreamReader;

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;

public class JobRepositoryInitializer {
	private DataSource dataSource;
	private String configFile;
	
	/**
	 * This method reads the schema from an xml file and determines
	 * if it needs to issue DDL statements to create or modify tables
	 * already in the database.
	 * 
	 * @throws Exception
	 */
	public void checkDDL() throws Exception {
        Platform platform = PlatformFactory
        .createNewPlatformInstance(dataSource);

        // Apache DDLUtils framework is used to create/modify schema.
		Database database = new DatabaseIO().read(new InputStreamReader(
		        getClass().getResourceAsStream(configFile)));
		
		platform.alterTables(database, false);
		System.out.println("HSQLDB is ready: " + platform);
	}

	public void shutdownDatabase() {
        SimpleJdbcTemplate template = new SimpleJdbcTemplate(dataSource);
        template.update("SHUTDOWN;");
        System.out.println("HSQLDB was gracefully shutdown. ");
	}
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public DataSource getDataSource() {
		return dataSource;
	}

	public void setConfigFile(String configFile) {
		this.configFile = configFile;
	}

	public String getConfigFile() {
		return configFile;
	}	
}

Configuration

In order to get the application to use HSQLDB and generate the JobRepository DDL Schema you need to do the following.

Configure the database

The following bean allows you to define a dataSource that will be used to connect to the HSQLDB database. The database files will be located in the src/main/resources/db folder. (This is specified in the database URL property)

src/main/resources/applicationContext.xml

<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/resources/db/testdb"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
</bean>

Job Repository Configuration

Update the configuration for the job repository to look like this:

src/main/resources/applicationContext.xml

    <beans:bean id="jobRepository"
        class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean">
        <beans:property name="databaseType" value="HSQL"/>
        <beans:property name="dataSource" ref="dataSource"/>
        <beans:property name="transactionManager" ref="transactionManager" />
    </beans:bean>

Apache DDLUtil Configuration

When the application starts for the first time the system will need to create the JobRepository tables. The following Spring Bean initializes the job repository and creates tables that are defined in the
src/main/resources/applicationContext.xml

	<beans:bean id="jobRepositoryInitializer" class="com.test.JobRepositoryInitializer"
		init-method="checkDDL" destroy-method="shutdownDatabase">
		<beans:property name="dataSource" ref="dataSource" />
		<beans:property name="configFile" value="/ddl.xml" />
	</beans:bean>

Run the application

To run the job from the command line type the following:

mvn exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"

You will notice that if you run the job again the console will print the following:

SEVERE: Job Terminated in error: A job instance already exists and is complete for parameters={}. If you want to run this job again, change the parameters.
org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={}. If you want to run this job again, change the parameters.

This is a successful indication that the HSQLDB is working persisting the data. Spring Batch framework is indicating that the helloWorld job invocation has already happened. More than one invocation with the same parameters is not allowed. If you want to read more about this then please read the following article.

You can also read the database script file located here:

src/main/resources/db/testdb.script

INSERT INTO BATCH_JOB_INSTANCE VALUES(0,0,'helloWorldJob','d41d8cd98f00b204e9800998ecf8427e')
INSERT INTO BATCH_JOB_EXECUTION VALUES(0,2,0,'2011-10-02 19:53:54.601000000','2011-10-02 19:53:54.617000000','2011-10-02 19:53:54.642000000','COMPLETED','COMPLETED','','2011-10-02 19:53:54.642000000')
INSERT INTO BATCH_STEP_EXECUTION VALUES(0,3,'step1',0,'2011-10-02 19:53:54.626000000','2011-10-02 19:53:54.639000000','COMPLETED',1,0,0,0,0,0,0,0,'COMPLETED','','2011-10-02 19:53:54.639000000')
INSERT INTO BATCH_STEP_EXECUTION_CONTEXT VALUES(0,'{"map":""}',NULL)
INSERT INTO BATCH_JOB_EXECUTION_CONTEXT VALUES(0,'{"map":""}',NULL)
INSERT INTO BATCH_STEP_EXECUTION_SEQ VALUES(0)
INSERT INTO BATCH_JOB_EXECUTION_SEQ VALUES(0)
INSERT INTO BATCH_JOB_SEQ VALUES(0)

HSQLDB uses this file to persist data to the file system in-between batch invocations.

That’s all for now!

01
Oct
11

Spring Batch Headers and Footers

This page describes the process of creating running a Spring Batch job that creates an output file with header and footer information.

Background

In prior articles we created a batch job that read information and outputted the result to another file. The output file was missing a header and footer lines. In this article we will look into generating this information.

Requirements

  • Java 5
  • Maven 2
  • This page picks up where the following page left off. Please Review and Implement the following article.

Header

The header of the will simply contain the filename and the timestamp.

fileName,Timestamp

Footer

The footer contained in the output be composed of 2 fields.

#ofRecords,salarySum

where:
#ofRecords – Count of the number of records in the output
salarySum – Sum of all values in the salary field

Solution

Create a Item Writer that will wrap the “FlatFileItemWriter”. The “headerCallback” and “footerCallback” methods of the ItemWriter will be called at the beginning and ending of the file. This allows developers to implement code that will allow them to write information to the header and footer of the output file.

src/main/java/com/test/EmployeeItemWriter.java

package com.test;

import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.item.ItemStreamException;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileFooterCallback;
import org.springframework.batch.item.file.FlatFileHeaderCallback;
import org.springframework.batch.item.file.FlatFileItemWriter;

public class EmployeeItemWriter implements ItemWriter<Employee>,
		FlatFileFooterCallback, FlatFileHeaderCallback, ItemStream {

    private FlatFileItemWriter<Employee> delegate;

    private BigDecimal totalAmount = BigDecimal.ZERO;

	private int recordCount = 0;
    
    public void writeFooter(Writer writer) throws IOException {
        writer.write(""+recordCount + "," + totalAmount);
    }
	public void writeHeader(Writer writer) throws IOException {
        writer.write("output_file.txt" + "," + new Date());
	}
    public void write(List<? extends Employee> items) throws Exception {
        BigDecimal chunkTotal = BigDecimal.ZERO;
        int chunkRecord = 0;
        for (Employee employee : items) {
            chunkRecord++;
            chunkTotal = chunkTotal.add(new BigDecimal(employee.getSalary()));
        }
        delegate.write(items);
        // After successfully writing all items
        totalAmount = totalAmount.add(chunkTotal);
        recordCount += chunkRecord;
	}
    
    public void setDelegate(FlatFileItemWriter<Employee> delegate) {
		this.delegate = delegate;
	}

	public void close() throws ItemStreamException {
		this.delegate.close();
	}

	public void open(ExecutionContext arg0) throws ItemStreamException {
		this.delegate.open(arg0);
	}

	public void update(ExecutionContext arg0) throws ItemStreamException {
		this.delegate.update(arg0);
	}
}

Batch XML Configuration

The following xml will allow the ItemWriter to wrap the FlatFileItemWriter and maintain a running total.

The highlighted lines are the changes from the prior version.

/src/main/resources/simpleJob.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/batch"
     xmlns:beans="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="

http://www.springframework.org/schema/batch


http://www.springframework.org/schema/batch/spring-batch-2.1.xsd


http://www.springframework.org/schema/beans


http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

    <beans:import resource="applicationContext.xml"/>
 
<!-- Tokenizer - Converts a delimited string into a Set of Fields -->
<beans:bean name="defaultTokenizer" 
    class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"/>

<!-- FieldSetMapper - Populates a bean's attributes with using the FieldSet -->
<beans:bean name="employeeFieldSetMapper" class="com.test.EmployeeFieldSetMapper"/>

<!-- LineMapper - Uses the tokenizer and Mapper to create instances of a Bean. -->
<beans:bean name="employeeLineMapper" class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
    <beans:property name="lineTokenizer" ref="defaultTokenizer"/>    
    <beans:property name="fieldSetMapper" ref="employeeFieldSetMapper"/>        
</beans:bean>

<!-- Reader - used by the tasklet to process one Item from the input. -->
<beans:bean name="empReader" class="org.springframework.batch.item.file.FlatFileItemReader">
    <beans:property name="lineMapper" ref="employeeLineMapper"/>
    
    <!-- use spring integrations for the following, but for now filename is hard coded -->
    <beans:property name="resource" value="input_data.txt"/>    
</beans:bean>

<!-- Processor -->

<beans:bean name="empProcessor" class="com.test.EmployeeProcessor">
</beans:bean>

<!-- Writer -->
<beans:bean id="empWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
    <beans:property name="resource" value="file:target/output_data.txt" />
    <beans:property name="lineAggregator">
        <beans:bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
            <beans:property name="delimiter" value=","/>
            <beans:property name="fieldExtractor">
                <beans:bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
                    <beans:property name="names" value="empId,lastName,title,salary,rank"/>
                </beans:bean>
            </beans:property>
        </beans:bean>
    </beans:property>
    <beans:property name="footerCallback" ref="empHeaderFooterWriter" />
    <beans:property name="headerCallback" ref="empHeaderFooterWriter" />
</beans:bean>

<beans:bean id="empHeaderFooterWriter" class="com.test.EmployeeItemWriter">
    <beans:property name="delegate" ref="empWriter"/>
</beans:bean>

<job id="helloWorldJob">
    <step id="step1" next="step2">
        <tasklet ref="helloWorldTasklet" />
    </step>
    <step id="step2">
        <tasklet>
            <chunk reader="empReader" processor="empProcessor" writer="empHeaderFooterWriter" commit-interval="1"/>
        </tasklet>
    </step>
</job>
 
<beans:bean name="helloWorldTasklet" class="com.test.HelloWorldTasklet"/>
 
<!--
To run the job from the command line type the following:
mvn exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"
 -->
</beans:beans>

Execute the job

Go to the command line and type the following:

mvn exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"

View the results

The output file will appear in the target/ folder of the project.

The file should look like this:

output_file.txt,Sat Oct 01 22:50:17 EDT 2011
7876,ADAMS,CLERK,1100,N/A
7499,ALLEN,SALESMAN,1600,N/A
7698,BLAKE,MANAGER,2850,Director
7782,CLARK,MANAGER,2450,N/A
7902,FORD,ANALYST,3000,Director
7900,JAMES,CLERK,950,N/A
7566,JONES,MANAGER,2975,Director
7839,KING,PRESIDENT,5000,Director
7654,MARTIN,SALESMAN,1250,N/A
7934,MILLER,CLERK,1300,N/A
7788,SCOTT,ANALYST,3000,Director
7369,SMITH,CLERK,800,N/A
7844,TURNER,SALESMAN,1500,N/A
7521,WARD,SALESMAN,1250,N/A
14,29025

That’s all for now!

01
Oct
11

The 3 R’s of Spring Batch

This page describes how you can Read, wRrite and perform aRithmetic on flat files using The Spring Batch Framework. We will take a comma separated file (csv) that contain employee information, add some information to it, and write it back to the file system.

Background

The basic building blocks of any batch process is

  1. Reading a Item
  2. Performing an operation on it
  3. Writing the Item back

Please take some time to review The Domain Language of Batch before proceeding. It covers much of the fundamental concepts we will be covering here.

Batch Steps

This page is focused on an individual step of the batch process.

The following is from the spring batch documentation

A Step is a domain object that encapsulates an independent, sequential phase of a batch job. Therefore, every Job is composed entirely of one or more steps. A Step contains all of the information necessary to define and control the actual batch processing. This is a necessarily vague description because the contents of any given Step are at the discretion of the developer writing a Job. A Step can be as simple or complex as the developer desires. A simple Step might load data from a file into the database, requiring little or no code. (depending upon the implementations used) A more complex Step may have complicated business rules that are applied as part of the processing.

Step Processing types

There are 2 ways a step can process data,

Tasklet

If the step requires only to execute a single task then you can use a tasklet. Typical use case for this is when you need to run a stored procedure, or copy a file from one location to the other. In the “Hello World” example we used a Tasklet to print the message to the console.

Chunk oriented

Chunk oriented processing involves specifying a reader, processor and writer. The input is read one item at a time in sequence and passed to the processor and eventually to the writer in chunks within a transaction boundary. Once the commit interval is reached the items are committed to the writer. Chunk oriented processing is what we will cover on this page.

Requirements

Project Setup

We will be modifying an existing project so please review the articles listed in the section above.

Input Data

The following is the input csv file that will be read. Please create the following file in the projects resource directory.

src/main/resources/input_data.txt

7876,ADAMS,CLERK,1100
7499,ALLEN,SALESMAN,1600
7698,BLAKE,MANAGER,2850
7782,CLARK,MANAGER,2450
7902,FORD,ANALYST,3000
7900,JAMES,CLERK,950
7566,JONES,MANAGER,2975
7839,KING,PRESIDENT,-5000
7654,MARTIN,SALESMAN,1250
7934,MILLER,CLERK,1300
7788,SCOTT,ANALYST,3000
7369,SMITH,CLERK,800
7844,TURNER,SALESMAN,1500
7521,WARD,SALESMAN,1250

Employee Bean

This is a simple bean that represents a single Employee.

src/main/java/com/test/Employee.java

package com.test;

public class Employee {

	private Integer empId;
	private String lastName;
	private String title;
	private Integer salary;
	private String rank;
	
	public Integer getEmpId() {
		return empId;
	}
	public void setEmpId(Integer empId) {
		this.empId = empId;
	}
	public String getLastName() {
		return lastName;
	}
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public Integer getSalary() {
		return salary;
	}
	public void setSalary(Integer salary) {
		this.salary = salary;
	}
	public void setRank(String rank) {
		this.rank = rank;
	}
	public String getRank() {
		return rank;
	}
	@Override
	public String toString() {
		return "Employee [empId=" + empId + ", lastName=" + lastName
				+ ", title=" + title + ", salary=" + salary + ", rank=" + rank
				+ "]";
	}	
}

Reading

In order to read data from a file we will only need to write the FieldSetMapper class that takes a FieldSet object and maps its contents into an bean.

src/main/java/com/test/EmployeeFieldSetMapper.java

package com.test;

import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
import org.springframework.validation.BindException;

public class EmployeeFieldSetMapper implements FieldSetMapper<Employee> {

	public Employee mapFieldSet(FieldSet fieldSet) throws BindException {
		if(fieldSet == null) return null;
		
		Employee emp = new Employee();	
		// unlike jdbc the index is 0 based	
		emp.setEmpId(fieldSet.readInt(0)); 
		emp.setLastName(fieldSet.readString(1));
		emp.setTitle(fieldSet.readString(2));
		emp.setSalary(fieldSet.readInt(3));
		
		return emp;
	}

}

Arithmetic

Not really! All we are doing is assigning a Rank based on the salary amount. The item processor takes an input Bean and converts it to an output bean. In this case the beans are the same but they don’t have to be.

src/main/java/com/test/EmployeeProcessor.java

package com.test;

import org.springframework.batch.item.ItemProcessor;

public class EmployeeProcessor implements ItemProcessor<Employee, Employee> {

	public Employee process(Employee emp) throws Exception {
		// if salary >= 2500 then set rank as "Director"		
		if(emp.getSalary() >= 2500 ) {
			emp.setRank("Director");			
		} else {
			emp.setRank("N/A");
		}
		return emp;
	}

}

Writing

All the objects needed for writing the file are configured using xml. See the xml file below for more details.

Job Configuration

Modify the job xml file to define the new beans.

src/main/resources/simpleJob.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/batch"
     xmlns:beans="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="

http://www.springframework.org/schema/batch


http://www.springframework.org/schema/batch/spring-batch-2.1.xsd


http://www.springframework.org/schema/beans


http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

    <beans:import resource="applicationContext.xml"/>
 
<!-- Tokenizer - Converts a delimited string into a Set of Fields -->
<beans:bean name="defaultTokenizer" 
    class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"/>

<!-- FieldSetMapper - Populates a bean's attributes with using the FieldSet -->
<beans:bean name="employeeFieldSetMapper" class="com.test.EmployeeFieldSetMapper"/>

<!-- LineMapper - Uses the tokenizer and Mapper to create instances of a Bean. -->
<beans:bean name="employeeLineMapper" class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
    <beans:property name="lineTokenizer" ref="defaultTokenizer"/>    
    <beans:property name="fieldSetMapper" ref="employeeFieldSetMapper"/>        
</beans:bean>

<!-- Reader - used by the tasklet to process one Item from the input. -->
<beans:bean name="empReader" class="org.springframework.batch.item.file.FlatFileItemReader">
    <beans:property name="lineMapper" ref="employeeLineMapper"/>
    
    <!-- use spring integrations for the following, but for now filename is hard coded -->
    <beans:property name="resource" value="input_data.txt"/>    
</beans:bean>

<!-- Processor -->

<beans:bean name="empProcessor" class="com.test.EmployeeProcessor">
</beans:bean>

<!-- Writer -->
<beans:bean id="empWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
    <beans:property name="resource" value="file:target/output_data.txt" />
    <beans:property name="lineAggregator">
        <beans:bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
            <beans:property name="delimiter" value=","/>
            <beans:property name="fieldExtractor">
                <beans:bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
                    <beans:property name="names" value="empId,lastName,title,salary,rank"/>
                </beans:bean>
            </beans:property>
        </beans:bean>
    </beans:property>
</beans:bean>

<job id="helloWorldJob">
    <step id="step1" next="step2">
        <tasklet ref="helloWorldTasklet" />
    </step>
    <step id="step2">
        <tasklet>
            <chunk reader="empReader" processor="empProcessor" writer="empWriter" commit-interval="1"/>
        </tasklet>
    </step>
</job>
 
<beans:bean name="helloWorldTasklet" class="com.test.HelloWorldTasklet"/>
 
<!--
To run the job from the command line type the following:
mvn clean compile exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"
 -->
</beans:beans>

Execute the job

Go to the command line and type the following:

mvn clean compile exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"

View the Results

The output file will appear in the target/ folder of the project.

Further Reading

To keep things simple we were reading and writing files located in the project own folders. There are many enterprise design patterns that describe the best practices for feeding data into the batch programs. For further reading on this topic please see the Spring Integrations Framework Homepage.

01
Oct
11

Hello World With Spring Batch 2.1.x

This page describes how convert the spring hello world example application to work using spring batch 2.1.x.

Background

The Hello World With Spring Batch was written a while ago. Since then there was a new version of spring batch released. Instead of changing the original article, I chose to write a new one just describing the changes needed to get the application described there to work.

The original article is located here:

http://numberformat.wordpress.com/2010/02/05/hello-world-with-spring-batch/

Step 1

Update the pom.xml dependency for spring-batch-core.

pom.xml

        <dependency>
            <groupId>org.springframework.batch</groupId>
            <artifactId>spring-batch-core</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>

Step 2

update the xml:

src/main/resources/simpleJob.xml


http://www.springframework.org/schema/batch/spring-batch-2.0.xsd">

with


http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">

Finally Run the command just like before:

mvn clean compile exec:java -Dexec.mainClass=org.springframework.batch.core.launch.support.CommandLineJobRunner -Dexec.args="simpleJob.xml helloWorldJob"

That’s all for now!

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: http://numberformat.wordpress.com/2011/04/07/restful-crud-using-extjs/




Follow

Get every new post delivered to your Inbox.

Join 49 other followers