Posts Tagged ‘crud

28
Feb
16

HTML 5 Responsive AJAX based CRUD application using Spring 4, JPA 2, JSF 2

This page shows how to create a transactional AJAX based HTML 5 responsive CRUD application to manage a company addressbook. In addition we will using annotations wherever possible. For ease of use we will use a HSQLDB in memory database that will automatically create a directory called data in your userid’s home directory.

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

Requirements

  • Java 8
  • Spring 4
  • JPA 2
  • JSF 2.1
  • HSQLDB
  • JAVA EE 6 level containers like JBoss EAP 6.4 or Tomcat 7

Create the pom.xml

First step is to create a pom.xml file. If you are using Tomcat 7 then enable the Glassfish dependency. Since Tomcat is only a servlet engine you will need implementation classes that don’t exist in Tomcat but are provided by other more beefy web application servers.

vi pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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>JSFJPASpringCRUDBasic</artifactId>
	<version>20160228</version>
	<packaging>war</packaging>
	<name>JSFJPASpringCRUDBasic</name>
	<pluginRepositories>
		<pluginRepository> <!-- Ignore this repository. Its only used for document publication. -->
			<id>numberformat-releases</id>
			<url>https://raw.githubusercontent.com/numberformat/wordpress/master/20130213/repo/</url>
		</pluginRepository>
	</pluginRepositories>
	<!-- It is possible to accidentally mix different versions of Spring JARs 
		when using Maven. For example, you may find that a third-party library, or 
		another Spring project, pulls in a transitive dependency to an older release. 
		If you forget to explicitly declare a direct dependency yourself, all sorts 
		of unexpected issues can arise. To overcome such problems Maven supports 
		the concept of a "bill of materials" (BOM) dependency. You can import the 
		spring-framework-bom in your dependencyManagement section to ensure that 
		all spring dependencies (both direct and transitive) are at the same version. 
		An added benefit of using the BOM is that you no longer need to specify the 
		<version> attribute when depending on Spring Framework artifacts -->
	<dependencyManagement>
	    <dependencies>
	        <dependency>
	            <groupId>org.springframework</groupId>
	            <artifactId>spring-framework-bom</artifactId>
	            <version>4.1.4.RELEASE</version>
	            <type>pom</type>
	            <scope>import</scope>
	        </dependency>
	    </dependencies>
	</dependencyManagement>

	<repositories>
		<repository>
			<id>prime-repo</id>
			<name>PrimeFaces Maven Repository</name>
			<url>http://repository.primefaces.org</url>
			<layout>default</layout>
		</repository>
	</repositories>
	
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
		</dependency>
<!-- Logging -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.1</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>1.7.1</version>
		</dependency>
<!-- Data Management -->
		<dependency>
			<groupId>org.hibernate.javax.persistence</groupId>
			<artifactId>hibernate-jpa-2.0-api</artifactId>
			<version>1.0.1.Final</version>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-entitymanager</artifactId>
			<version>4.1.6.Final</version>
		</dependency>
		<dependency>
			<groupId>org.hsqldb</groupId>
			<artifactId>hsqldb</artifactId>
			<version>2.3.3</version>
		</dependency>		
<!-- Spring Framework -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-beans</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-orm</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
		</dependency>
<!-- JSF -->
		<dependency>
			<groupId>net.bootsfaces</groupId>
			<artifactId>bootsfaces</artifactId>
			<version>0.8.0</version>
		</dependency>
		<dependency>
			<groupId>org.primefaces</groupId>
			<artifactId>primefaces</artifactId>
			<version>5.3</version>
		</dependency>
		<dependency>
			<groupId>org.primefaces.themes</groupId>
			<artifactId>bootstrap</artifactId>
			<version>1.0.9</version>
		</dependency>
		<!-- If running this in a Servlet only container like Tomcat 7 enable this dependency 
		<dependency> 
			<groupId>org.glassfish</groupId>
			<artifactId>javax.faces</artifactId>
			<version>2.1.6</version>
		</dependency>		
		-->
<!-- APIs for services provided by the containers -->		
		<dependency>
			<groupId>javax</groupId>
			<artifactId>javaee-web-api</artifactId>
			<version>6.0</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>net.sf.opencsv</groupId>
			<artifactId>opencsv</artifactId>
			<version>2.3</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<plugin> <!-- Ignore this plugin. Its only used for document publication. -->
				<groupId>github.numberformat</groupId>
				<artifactId>blog-plugin</artifactId>
				<version>1.0-SNAPSHOT</version>
				<configuration>
					<gitUrl>https://github.com/numberformat/wordpress/tree/master/${project.version}/${project.artifactId}</gitUrl>
				</configuration>
				<executions>
					<execution>
						<id>1</id>
						<phase>site</phase>
						<goals>
							<goal>generate</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>

Web Application Initialization

This app depends on the Servlet 3.0 specification. The spec requires web application containers to search the classpath looking for Initializers. Classes that implement the the spring “WebApplicationInitializer” are the ones that will be picked up.

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

package com.test;  
  
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;  

/**
 * The Servlet 3 containers automatically search the classpath for classes that
 * implement the below interface and call the onStartup() method on initialization.
 */
public class WebAppInitializer implements WebApplicationInitializer {  
      
    @Override  
    public void onStartup(ServletContext servletContext) throws ServletException {  
    	// Initialize the spring framework.
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();  
        ctx.register(SpringConfig.class);  
        ctx.setServletContext(servletContext);    
        servletContext.addListener(new ContextLoaderListener(ctx));
        // required to get spring beans injected into JSF managed beans
        servletContext.addListener(new RequestContextListener());
        
        // initialize faces (not working...)
//        servletContext.setInitParameter("primefaces.THEME", "bootstrap");
//        servletContext.setInitParameter("javax.faces.DEFAULT_SUFFIX", ".xhtml");
//        ServletRegistration.Dynamic faces = servletContext.addServlet("Faces Servlet", "javax.faces.webapp.FacesServlet");
//        faces.setLoadOnStartup(1);
//        faces.addMapping("*.xhtml");
        
    }  
  
}  

Spring Configuration (No XML)

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

package com.test;  
  
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.faces.context.FacesContext;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.CustomScopeConfigurer;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;  

/**
 * This class replaces the spring xml configuration.
 * 
 */
@Configuration
@ComponentScan("com.test")
@EnableTransactionManagement 
public class SpringConfig {
	
	@Bean
	public DataSource getDataSource() {
		DriverManagerDataSource dm = new DriverManagerDataSource();
		dm.setDriverClassName("org.hsqldb.jdbcDriver");
		dm.setUrl("jdbc:hsqldb:file:"+System.getProperty("user.home") + "/data/JSFJPASpringCRUD"+";shutdown=true");
		dm.setUsername("sa");
		return dm;
	}
	
//	@Bean
//	public DataSource getDataSource() {
//		DriverManagerDataSource dm = new DriverManagerDataSource();
//		dm.setDriverClassName("com.mysql.jdbc.Driver");
//		// change the "/tmp" to be another location on your system.
//		dm.setUrl("jdbc:mysql://localhost:3306/xxxx");
//		dm.setUsername("xxxx");
//		dm.setPassword("xxxx");
//		return dm;
//	}
	
	@Bean
	public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
		LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
		em.setDataSource(getDataSource());
		em.setPackagesToScan(new String[] { "com.test.model" });
		JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
		em.setJpaVendorAdapter(vendorAdapter);
		em.setJpaProperties(additionalProperties());
		return em;
	}

	@Bean
	public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
		JpaTransactionManager transactionManager = new JpaTransactionManager();
		transactionManager.setEntityManagerFactory(emf);
		return transactionManager;
	}

	@Bean
	public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
		return new PersistenceExceptionTranslationPostProcessor();
	}

	Properties additionalProperties() {
		Properties properties = new Properties();
		properties.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
//		properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL5InnoDBDialect");
		properties.setProperty("hibernate.flushMode", "FLUSH_AUTO");
		properties.setProperty("hibernate.hbm2ddl.auto", "update");
		properties.setProperty("hibernate.show_sql", "false");
		return properties;
	}  

	@Bean
	public CustomScopeConfigurer customScope () {
	    CustomScopeConfigurer configurer = new CustomScopeConfigurer ();
	    Map<String, Object> viewScope = new HashMap<String, Object>();
	    viewScope.put("view", new ViewScope());
	    configurer.setScopes(viewScope);

	    return configurer;
	}
	
	class ViewScope implements Scope {
		 
	    public Object get(String name, @SuppressWarnings("rawtypes") ObjectFactory objectFactory) {
	        Map<String,Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
	 
	        if(viewMap.containsKey(name)) {
	            return viewMap.get(name);
	        } else {
	            Object object = objectFactory.getObject();
	            viewMap.put(name, object);
	 
	            return object;
	        }
	    }
	 
	    public Object remove(String name) {
	        return FacesContext.getCurrentInstance().getViewRoot().getViewMap().remove(name);
	    }
	 
	    public String getConversationId() {
	        return null;
	    }
	 
	    public void registerDestructionCallback(String name, Runnable callback) {
	        //Not supported
	    }
	 
	    public Object resolveContextualObject(String key) {
	        return null;
	    }
	}
	
}  

Logging configuration

vi src/main/resources/log4j.xml

<!DOCTYPE log4j:configuration PUBLIC
  "-//APACHE//DTD LOG4J 1.2//EN" "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
   
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    
	<appender name="file" class="org.apache.log4j.FileAppender">
	   <param name="maxFileSize" value="1024KB" />
	   <param name="file" value="${user.home}/logs/JSFJPASpringCRUDBasic.log" />
	   <layout class="org.apache.log4j.PatternLayout">
		<param name="ConversionPattern" 
			value="%d %-5p %c:%L - %m%n" />
	   </layout>
	</appender>
	    
    <logger name="com.test">
        <level value="DEBUG"/>
    </logger>
    <logger name="com.springframework">
        <level value="INFO"/>
    </logger>
    <logger name="org.hibernate.SQL">
        <level value="DEBUG"/>
    </logger>
    <root>
        <level value="ERROR"/>
        <appender-ref ref="file"/>
    </root> 
</log4j:configuration>

Data Managers and Entities

Used by JPA to define and interact with the database.

vi src/main/java/com/test/model/Company.java

package com.test.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="company")
public class Company {
	@Id
	@GeneratedValue
	@Column(name="company_id")
	private Long id;
	@Column(name="name")
	private String name;
	@Column(name="shipto_address1")
	private String shipToAddress;
	@Column(name="shipto_address2")
	private String shipToAddress2;
	@Column(name="shipto_city")
	private String shipToCity;
	@Column(name="shipto_provence")
	private String shipToProvence;
	@Column(name="shipto_zip")
	private String shipToZip;
	@Column(name="cosign_address1")
	private String coSignAddress;
	@Column(name="cosign_address2")
	private String coSignAddress2;
	@Column(name="cosign_city")
	private String coSignCity;
	@Column(name="cosign_zip")
	private String coSignZip;
	@Column(name="cosign_country")
	private String coSignCountry;

	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getShipToAddress() {
		return shipToAddress;
	}
	public void setShipToAddress(String shipToAddress) {
		this.shipToAddress = shipToAddress;
	}
	public String getShipToAddress2() {
		return shipToAddress2;
	}
	public void setShipToAddress2(String shipToAddress2) {
		this.shipToAddress2 = shipToAddress2;
	}
	public String getShipToCity() {
		return shipToCity;
	}
	public void setShipToCity(String shipToCity) {
		this.shipToCity = shipToCity;
	}
	public String getShipToProvence() {
		return shipToProvence;
	}
	public void setShipToProvence(String shipToProvence) {
		this.shipToProvence = shipToProvence;
	}
	public String getShipToZip() {
		return shipToZip;
	}
	public void setShipToZip(String shipToZip) {
		this.shipToZip = shipToZip;
	}
	public String getCoSignAddress() {
		return coSignAddress;
	}
	public void setCoSignAddress(String coSignAddress) {
		this.coSignAddress = coSignAddress;
	}
	public String getCoSignAddress2() {
		return coSignAddress2;
	}
	public void setCoSignAddress2(String coSignAddress2) {
		this.coSignAddress2 = coSignAddress2;
	}
	public String getCoSignCity() {
		return coSignCity;
	}
	public void setCoSignCity(String coSignCity) {
		this.coSignCity = coSignCity;
	}
	public String getCoSignZip() {
		return coSignZip;
	}
	public void setCoSignZip(String coSignZip) {
		this.coSignZip = coSignZip;
	}
	public String getCoSignCountry() {
		return coSignCountry;
	}
	public void setCoSignCountry(String coSignCountry) {
		this.coSignCountry = coSignCountry;
	}
	
}

Spring DataManager Interface.

vi src/main/java/com/test/model/CompanyDataManager.java

package com.test.model;

import java.util.List;

public interface CompanyDataManager {
	// C
	Company create(Company selectedCompany);
	// R
	List<Company> getAll();
	// U
	Company update(Company selectedCompany);
	// D
	void delete(Company selectedCompany);
}

Spring DataManager Implementation

vi src/main/java/com/test/model/CompanyDataManagerImpl.java

package com.test.model;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.stereotype.Component;

@Component("companyDataManager")
public class CompanyDataManagerImpl implements CompanyDataManager {
	@PersistenceContext 
	private EntityManager em;

	@Override
	public List<Company> getAll() {
		return em.createQuery("SELECT c FROM Company c", Company.class).getResultList();
	}

	@Override
	public Company update(Company selectedCompany) {
		Company mergedCompany = em.merge(selectedCompany);
		return mergedCompany;
	}

	@Override
	public void delete(Company selectedCompany) {
		em.remove(em.contains(selectedCompany) ? selectedCompany : em.merge(selectedCompany));
	}
	
	public Company create(Company selectedCompany) {
		em.persist(selectedCompany);
		return selectedCompany;
	}
	
}

JSF Page and the JSF Managed Bean

The managed bean hold user entered data. This particular bean is in the View Scope. This ia scope between request and session. It stays active as long as the uesr is on the page. We could not use the Request Scope for this because the page makes AJAX call in the background.

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

package com.test;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Random;

import javax.faces.bean.ManagedBean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import com.test.model.Company;
import com.test.model.CompanyDataManager;

import au.com.bytecode.opencsv.CSVReader;


/**
 * The following is a JSF Managed Bean that is actually managed by Spring.
 */

@Controller
@Scope(value="view") // you can not use JSF Scoped values here since its a spring managed bean
@ManagedBean	// use this to indicate that its a JSF Bean (managed by spring)
public class CompanyMB {

	private static final Logger logger = LoggerFactory.getLogger(CompanyMB.class);

	@Autowired
	private CompanyDataManager companyDataManager;
	
	private Company selectedCompany;
	private List<Company> all;
	private Boolean addState = false;
	
	public List<Company> getAll() { // getters should NEVER implement DB fetch logic.
		if(all==null) loadAll();
		return all;
	}

	/**
	 * This method will be called by the page during preRenderView. Example:
	 * <f:event type="preRenderView" listener="#{companyMB.loadAll}"/>
	 */
	@Transactional(isolation=Isolation.SERIALIZABLE)
	public void loadAll() {
    	logger.info("getting All");
		if(companyDataManager==null) throw new NullPointerException("dataManager is null");
		all = companyDataManager.getAll();
	}

	public CompanyDataManager getCompanyDataManager() {
		return companyDataManager;
	}


	public Company getSelectedCompany() {
		return selectedCompany;
	}


	public void setSelectedCompany(Company selectedCompany) {
		this.selectedCompany = selectedCompany;
	}
	
	private int countLines(InputStream file) {
		if(file==null) throw new NullPointerException("file stream should not be null.");
		int lines = 0;
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(file))) {
			while (reader.readLine() != null) lines++;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return lines;
	}
	@Transactional
	public void newCompany() {
		Company c = getRandomCompany();
		selectedCompany = companyDataManager.create(c);
		addState = true;
	}

	private Company getRandomCompany() {
		final String DUMMY_COMPANY_LIST_CSV = "/dummyCompanyList.csv";

		Company c = new Company();
		InputStream is = getClass().getResourceAsStream(DUMMY_COMPANY_LIST_CSV);
		if(is==null) return c;
		int lines = countLines(is);
		int randomLine = new Random().nextInt(lines - 2) + 1;
		int index = 0;
		is = getClass().getResourceAsStream(DUMMY_COMPANY_LIST_CSV);
		try (CSVReader reader = new CSVReader(new InputStreamReader(is))) {
			String[] nextLine = null;
			while((nextLine=reader.readNext()) != null) {
				if(index >= randomLine) {
					c.setName(nextLine[0]);
					c.setShipToAddress(nextLine[3]);
					c.setShipToCity(nextLine[4]);
					c.setShipToProvence(nextLine[5]);
					c.setShipToZip(nextLine[6]);
					break;
				}
				index++;
			}
		} catch (Exception ex) {
			throw new RuntimeException("unable to read random address CSV file.", ex);
		}
		return c;
	}
	
	@Transactional
	public void updateSelectedCompany() {
		logger.debug("update Selected Company called.");
		companyDataManager.update(selectedCompany);
	}
	@Transactional
	public void deleteSelectedCompany() {
		logger.debug("delete Selected Company called.");
		companyDataManager.delete(selectedCompany);
	}
	
	@Transactional
	public void cancel() {
		if(addState) {
			deleteSelectedCompany();
			addState=false;
		}
	}
}

This is the only UI page the users will see. Single page HTML5 applications use javascript to show dialogs and submit requests in the background.

vi src/main/webapp/company.xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:b="http://bootsfaces.net/ui"
	xmlns:f="http://java.sun.com/jsf/core"
	xmlns:h="http://java.sun.com/jsf/html"
	xmlns:p="http://primefaces.org/ui">
<h:head>
	<title>Acme Distribution</title>
</h:head>
<h:body>
	<b:container>
		<b:navBar brand="Acme Wholesale Distributor" brandHref="#"
			inverse="true">
			<b:navbarLinks>
				<b:dropMenu value="Home" href="index.xhtml">
					<b:navLink value="Companies" href="company.xhtml"></b:navLink>
				</b:dropMenu>
			</b:navbarLinks>
		</b:navBar>

		<b:panel title="Company Address List" look="primary">

			<h:form id="form">
				<p:dataTable id="companyDt" var="company" value="#{companyMB.all}">
					<f:event type="preRenderView" listener="#{companyMB.loadAll}" />
					<p:column headerText="Name">
						<h:outputText value="#{company.name}" />
					</p:column>

					<p:column headerText="Address" priority="2">
						<h:outputText value="#{company.shipToAddress}" />
					</p:column>
					<p:column headerText="City" priority="3" width="160">
						<h:outputText value="#{company.shipToCity}" />
					</p:column>
					<p:column headerText="State" priority="4" width="160">
						<h:outputText value="#{company.shipToProvence}" />
					</p:column>
					<p:column headerText="Zip" priority="5" width="80">
						<h:outputText value="#{company.shipToZip}" />
					</p:column>
					<p:column width="55">
						<f:facet name="header">
						<p:commandButton action="#{companyMB.newCompany}" update=":form:companyDetail"
							oncomplete="PF('companyDialog').show()" icon="ui-icon-plusthick"
							title="Add"/>
						</f:facet>					
						<p:commandButton update=":form:companyDetail"
							oncomplete="PF('companyDialog').show()" icon="ui-icon-search"
							title="View">
							<f:setPropertyActionListener value="#{company}"
								target="#{companyMB.selectedCompany}" />
						</p:commandButton>
					</p:column>

				</p:dataTable>
				<!-- AJAX like Pop-up Dialog -->
				<p:dialog header="Company Info" widgetVar="companyDialog"
					modal="true" showEffect="fade" hideEffect="fade" resizable="false" closable="false" >
					
					<p:outputPanel id="companyDetail" style="text-align:center;">
						<p:panelGrid columns="2"
							rendered="#{not empty companyMB.selectedCompany}"
							columnClasses="label,value">
							<h:outputText value="Id:" />
							<h:outputText value="#{companyMB.selectedCompany.id}" />
							<h:outputText value="Name" />
							<h:inputText value="#{companyMB.selectedCompany.name}" />
							<h:outputText value="Address 1:" />
							<h:inputText value="#{companyMB.selectedCompany.shipToAddress}" />
							<h:outputText value="City:" />
							<h:inputText value="#{companyMB.selectedCompany.shipToCity}" />
							<h:outputText value="State:" />
							<h:inputText value="#{companyMB.selectedCompany.shipToProvence}" />
							<h:outputText value="Zip:" />
							<h:inputText value="#{companyMB.selectedCompany.shipToZip}" />
						</p:panelGrid>
						<!-- Save Button -->
						<p:commandButton update="companyDt" 
							action="#{companyMB.updateSelectedCompany}" value="Save"
							oncomplete="PF('companyDialog').hide()" icon="ui-icon-disk" />
						<!-- Delete Button -->
						<p:commandButton update="companyDt"
							action="#{companyMB.deleteSelectedCompany}" value="Delete"
							oncomplete="PF('companyDialog').hide()" icon="ui-icon-trash" />
						<p:commandButton action="#{companyMB.cancel}" update="companyDt" value="Cancel"
							oncomplete="PF('companyDialog').hide()"
							icon="ui-icon-arrowreturn-1-w" />
					</p:outputPanel>
				</p:dialog>
			</h:form>
		</b:panel>
		<b:panel>
		 <span class="subitem">Responsive</span>
                        <span class="defaultText dispTable">
        DataTable has two responsive modes. In priority mode, visibility of columns are toggled based on the screen size and their priority (1-6). On the other hand
        in reflow mode, columns are displayed as stacked on smaller screens.
    
                        </span>
		</b:panel>
	</b:container>
</h:body>
</html>

Sample Data (optional)

You can find the file under src/main/resources in the github repository for this page.

Advertisements
23
Apr
11

Nested JSON

This page describes the process of performing CRUD operations on Java Beans that have Nested Properties using ExtJS.

Requirements

The Item bean contains a length, width, and height property. We will move those attributes to a new bean called Dimension and add a dimension attribute to the Item class.

This will result in the editor Grid submitting the following xml in the Request Body:

<?xml version="1.0" encoding="UTF-8"?>
	<item>
		<itemId>5</itemId>
		<name>tes</name>
		<description>111111</description>
		<stockQty>2</stockQty>
		<color>blue</color>
		<dimension.length>1</dimension.length>
		<dimension.width>1</dimension.width>
		<dimension.height>1</dimension.height>
	</item>

The following changes need to be done:

  1. Create a new Dimension class, modify the Item class to use it.
  2. Modify the IBATIS sql map xml files
  3. Modify ExtJS JsonReader to read the nested information

src/main/java/com/test/Dimension.java

package com.test;

import java.math.BigDecimal;

public class Dimension {
    private BigDecimal length;
    private BigDecimal width;
    private BigDecimal height;
    
    
	public BigDecimal getLength() {
		return length;
	}
	public void setLength(BigDecimal length) {
		this.length = length;
	}
	public BigDecimal getWidth() {
		return width;
	}
	public void setWidth(BigDecimal width) {
		this.width = width;
	}
	public BigDecimal getHeight() {
		return height;
	}
	public void setHeight(BigDecimal height) {
		this.height = height;
	}
}

Item

Replace the implementation of Item.java with the following.

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

package com.test;
 
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlRootElement;
 
/**
 * Represents an item in inventory or in a shopping cart.
 */
@XmlRootElement
public class Item {
    private Integer itemId;
    private String oemId;
    private String name;
    private String description;
    private String imageURL;
    private String color;
    private BigDecimal price;
    private Dimension dimension = new Dimension();
     
    /**
     * Weight in grams
     */
    private BigDecimal weight;
    /**
     * Quantity in stock.
     */
    private Integer stockQty;
 
    public Item() {
        super();
    }
 
    public Item(Integer id, String name) {
        this.name = name;
        this.itemId = id;
    }
 
    public Integer getItemId() {
        return itemId;
    }
    public void setItemId(Integer itemId) {
        this.itemId = itemId;
    }
    public String getOemId() {
        return oemId;
    }
    public void setOemId(String oemId) {
        this.oemId = oemId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public String getImageURL() {
        return imageURL;
    }
    public void setImageURL(String imageURL) {
        this.imageURL = imageURL;
    }
    public BigDecimal getPrice() {
        return price;
    }
    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public BigDecimal getWeight() {
        return weight;
    }
    public void setWeight(BigDecimal weight) {
        this.weight = weight;
    }
 
    public Integer getStockQty() {
        return stockQty;
    }
 
    public void setStockQty(Integer stockQty) {
        this.stockQty = stockQty;
    }

    public void setDimension(Dimension dimension) {
		this.dimension = dimension;
    }

    public Dimension getDimension() {
		return dimension;
    }
}

IBATIS SQL Mapper

Change the SQL mapper configuration file to look like this…

src/main/resources/com/test/ItemDataManager.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
        PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
        "http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="Item">
 
<typeAlias alias="Item" type="com.test.Item" />

    <resultMap id="itemResultMap" class="Item">
		<result property="itemId" column="itemId"/>
		<result property="oemId" column="oemId"/>
		<result property="name" column="name"/>
		<result property="description" column="description"/>
		<result property="color" column="color"/>
		<result property="imageURL" column="imageURL"/>
		<result property="price" column="price"/>
		<result property="dimension.length" column="length"/>
		<result property="dimension.width" column="width"/>
		<result property="dimension.height" column="height"/>
		<result property="weight" column="weight"/>
		<result property="stockQty" column="stockQty"/>
    </resultMap>

 
    <select id="allItems" resultMap="itemResultMap" >
        select item_id as itemId,
        oem_id as oemId,
        item_x as name,
        description as description,
        color as color,
        image_url as imageURL,
        price as price,
        length as length,
        width as width,
        height as height,
        weight as weight,
        stock_qty as stockQty
        from item;
    </select>
    <select id="getItemInfo" parameterClass="int" resultMap="itemResultMap" >
        select item_id as itemId,
        oem_id as oemId,
        item_x as name,
        description as description,
        color as color,
        image_url as imageURL,
        price as price,
        length as length,
        width as width,
        height as height,
        weight as weight,
        stock_qty as stockQty
        from item where item_id = #id#;
    </select>
 
    <insert id="itemInsert" parameterClass="Item">
        insert into item (oem_id,
            item_x,
            description,
            color,
            image_url,
            price,
            length,
            width,
            height,
            weight,
            stock_qty)
        values ( #oemId#, #name#, #description#, #color#,
            #imageURL#, #price#, #dimension.length#, #dimension.width#,
            #dimension.height#, #weight#, #stockQty# );
        <selectKey resultClass="int" >
            <!-- HSQLDB -->
            CALL IDENTITY();
            <!-- MySQL -->
            <!-- SELECT LAST_INSERT_ID(); -->
        </selectKey>
    </insert>
    <update id="updateItem" parameterClass="Item">
        update item set oem_id = #oemId#, item_x = #name#, description=#description#,
            color = #color#, image_url = #imageURL#, price = #price#, length = #dimension.length#,
            width = #dimension.width#, height = #dimension.height#, weight = #weight#, stock_qty = #stockQty#
        where item_id = #itemId#;
    </update>
    <delete id="deleteItem" parameterClass="java.lang.Integer">
        delete from item where item_id = #itemId#;
    </delete>
</sqlMap>

Change the Grid

MyApp={};
MyApp.getContext = function() {
	var base = document.getElementsByTagName('base')[0];
	if (base && base.href && (base.href.length > 0)) {
		base = base.href;
	} else {
		base = document.URL;
	}
	return base.substr(0, base.indexOf("/", base.indexOf("/", base.indexOf("//") + 2) + 1));
};

var App = new Ext.App({});
 
var proxy = new Ext.data.HttpProxy({
	url: MyApp.getContext() + '/appdata/itemresource'
});

var reader = new Ext.data.JsonReader({
    totalProperty: 'total',
    successProperty: 'success',
    idProperty: 'itemId',
    root: 'data',
    record: 'item',
    messageProperty: 'message',  // <-- New "messageProperty" meta-data
    fields : [
              {name: 'itemId'},
              {name: 'name', allowBlank: false},
              {name: 'description', allowBlank: true},
              {name: 'stockQty', allowBlank: true},
              {name: 'color', allowBlank: true},
// The 'mapping' field allows binding to complex nested JSON objects              
              {name: 'length', allowBlank: true, mapping: 'dimension.length'},              
              {name: 'width', allowBlank: true, mapping: 'dimension.width'},              
              {name: 'height', allowBlank: true, mapping: 'dimension.height'}              
    ]
});

// The new DataWriter component.
var writer = new Ext.data.XmlWriter({
    encode : false,
    writeAllFields : true,
    xmlEncoding : "UTF-8"
});

// Typical Store collecting the Proxy, Reader and Writer together.
var store = new Ext.data.Store({
    id: 'item',
    restful: true,     // <-- This Store is RESTful
    proxy: proxy,
    reader: reader,
    writer: writer    // <-- plug a DataWriter into the store
});

// load the store
store.load();

// The following is necessary to map the Record Fields to the grid columns.
var userColumns =  [
    {header: "Item Id", width: 40, sortable: true, dataIndex: 'itemId'},
    {header: "Item Name", width: 100, sortable: true, dataIndex: 'name', editor: new Ext.form.TextField({})},
    {header: "Description", width: 100, sortable: true, dataIndex: 'description', editor: new Ext.form.TextField({})},
    {header: "Quantity", width: 100, sortable: true, dataIndex: 'stockQty', editor: new Ext.form.TextField({})},
    {header: "Color", width: 100, sortable: true, dataIndex: 'color', editor: new Ext.form.TextField({})},
    {header: "Length", width: 100, sortable: true, 
    	dataIndex: 'length', editor: new Ext.form.TextField({})},
    {header: "Width", width: 100, sortable: true, 
    	dataIndex: 'width', editor: new Ext.form.TextField({})},
    {header: "Height", width: 100, sortable: true, 
    	dataIndex: 'height', editor: new Ext.form.TextField({})}
];

Ext.onReady(function() {
    Ext.QuickTips.init();

    // use RowEditor for editing
    var editor = new Ext.ux.grid.RowEditor({
        saveText: 'Save'
    });

    // Create a typical GridPanel with RowEditor plugin
    var userGrid = new Ext.grid.GridPanel({
        renderTo: 'user-grid',
        iconCls: 'icon-grid',
        frame: true,
        title: 'Fun with RESTful CRUD',
        height: 300,
        store: store,
        plugins: [editor],
        columns : userColumns,
    	enableColumnHide: false,
        tbar: [{
        	id: 'addBtn',
            text: 'Add',
            iconCls: 'silk-add',
            handler: onAdd
        }, '-', {
        	id: 'deleteBtn',
            text: 'Delete',
            iconCls: 'silk-delete',
            handler: onDelete
        }, '-'],
        viewConfig: {
            forceFit: true
        }
    });

    function onAdd(btn, ev) {
        var u = new userGrid.store.recordType({
            first : '',
            last: '',
            email : ''
        });
        editor.stopEditing();
        userGrid.store.insert(0, u);
        editor.startEditing(0);
    }

    // Disable the buttons when editing.
    editor.addListener('beforeedit', function(roweditor, rowIndex){
        Ext.getCmp('addBtn').disable();
        Ext.getCmp('deleteBtn').disable();
    });
    editor.addListener('afteredit', function(roweditor, rowIndex){
        Ext.getCmp('addBtn').enable();
        Ext.getCmp('deleteBtn').enable();
    });
    editor.addListener('canceledit', function(roweditor, rowIndex){
    	// get fresh records from the database.
        userGrid.store.reload({});
        Ext.getCmp('addBtn').enable();
        Ext.getCmp('deleteBtn').enable();
    });     

    function onDelete() {
        var rec = userGrid.getSelectionModel().getSelected();
        if (!rec) {
            return false;
        }
        userGrid.store.remove(rec);
    }
    activateGridValidation(userGrid);
});

Test the application

In the project top folder type in the following command to compile and run the code in jetty servlet engine.

mvn clean compile jetty:run

Navigate to the following URL:
http://localhost:8080/crud/

Modify the application presented in Part II

In this section we will modify the application presented in part II of my posting in this series.

Regular form panel can submit nested bean information. The submission looks like this:

name=4&description=444444&stockQty=4&color=blue&dimension.length=4&dimension.width=4&dimension.height=4

Modify the form.html page to look like this…

src/main/webapp/form.html

<html>
<head>
 
<link rel="stylesheet" type="text/css"
    href="http://dev.sencha.com/deploy/ext-3.3.1/resources/css/ext-all.css"/>
 
<script type="text/javascript"
    src="http://dev.sencha.com/deploy/ext-3.3.1/adapter/ext/ext-base-debug.js">
</script>
<script type="text/javascript"
    src="http://dev.sencha.com/deploy/ext-3.3.1/ext-all-debug.js">
</script>
 
<script type="text/javascript">
 
MyApp={};
MyApp.getContext = function() {
    var base = document.getElementsByTagName('base')[0];
    if (base && base.href && (base.href.length > 0)) {
        base = base.href;
    } else {
        base = document.URL;
    }
    return base.substr(0, base.indexOf("/", base.indexOf("/", base.indexOf("//") + 2) + 1));
};
 
function buildWindow() {
    Ext.QuickTips.init();
    Ext.form.Field.prototype.msgTarget = 'side';
    var bd = Ext.getBody();
 
    var fs = new Ext.FormPanel({
        labelWidth: 75,
        frame: true,
        title:'Form with crazy amount of validation',
        bodyStyle:'padding:5px 5px 0',
        defaults: {width: 230},
        waitMsgTarget: true,
        defaultType: 'textfield',    
 
        items: [{
                fieldLabel: 'Item Name',
                name: 'name'
            },{
                fieldLabel: 'Description',
                name: 'description'
            },{
                fieldLabel: 'Quantity',
                name: 'stockQty'
            }, {
                fieldLabel: 'Color',
                name: 'color'
            }, {
                fieldLabel: 'Length',
                name: 'dimension.length'
		    }, {
		        fieldLabel: 'Width',
		        name: 'dimension.width'
			}, {
			    fieldLabel: 'Height',
			    name: 'dimension.height'
			}
        ],    
 
    });
 
    var onSuccessOrFail = function(form, action) {
        var result = action.result;
 
        if(result.success) {
            Ext.MessageBox.alert('Success', 'Success!');
            userGrid.store.reload({});
        } else { // put code here to handle form validation failure.
        }
 
    }
 
    var submit = fs.addButton({
        text: 'Submit',
        disabled:false,
        handler: function(){
            fs.getForm().submit({
                url:MyApp.getContext() +'/app/itemresource',
                waitMsg:'Submitting Data...',
                submitEmptyText: false,
                success : onSuccessOrFail,
                failure : onSuccessOrFail
            });
        }
    });
 
    fs.render('form-first');
 
    // the following is necessary to display the grid.
 
// The following is necessary to map the Record Fields to the grid columns.
var userColumns =  [
    {header: "Item Id", width: 40, sortable: true, dataIndex: 'itemId'},
    {header: "Item Name", width: 100, sortable: true, dataIndex: 'name', editor: new Ext.form.TextField({})},
    {header: "Description", width: 100, sortable: true, dataIndex: 'description', editor: new Ext.form.TextField({})},
    {header: "Quantity", width: 100, sortable: true, dataIndex: 'stockQty', editor: new Ext.form.TextField({})},
    {header: "Color", width: 100, sortable: true, dataIndex: 'color', editor: new Ext.form.TextField({})},
    {header: "Length", width: 100, sortable: true, 
    	dataIndex: 'length', editor: new Ext.form.TextField({})},
    {header: "Width", width: 100, sortable: true, 
    	dataIndex: 'width', editor: new Ext.form.TextField({})},
    {header: "Height", width: 100, sortable: true, 
    	dataIndex: 'height', editor: new Ext.form.TextField({})}
];
 
    var proxy = new Ext.data.HttpProxy({
        url: MyApp.getContext() + '/app/itemresource'
    });
 
    var reader = new Ext.data.JsonReader({
        totalProperty: 'total',
        successProperty: 'success',
        idProperty: 'itemId',
        root: 'data',
        record: 'item',
        messageProperty: 'message',  // <-- New "messageProperty" meta-data
        fields : [
                  {name: 'itemId'},
                  {name: 'name', allowBlank: false},
                  {name: 'description', allowBlank: true},
                  {name: 'stockQty', allowBlank: true},
              {name: 'color', allowBlank: true},
// The 'mapping' field allows binding to complex nested JSON objects              
              {name: 'length', allowBlank: true, mapping: 'dimension.length'},              
              {name: 'width', allowBlank: true, mapping: 'dimension.width'},              
              {name: 'height', allowBlank: true, mapping: 'dimension.height'}              
        ]
    });
 
    // Typical Store collecting the Proxy, Reader and Writer together.
    var store = new Ext.data.Store({
        id: 'item',
        restful: true,     // <-- This Store is RESTful
        proxy: proxy,
        reader: reader
    });
 // load the store
    store.load();
 
    // Create a typical GridPanel with RowEditor plugin
    var userGrid = new Ext.grid.GridPanel({
        renderTo: 'user-grid',
        iconCls: 'icon-grid',
        frame: true,
        title: 'Records in HyperSQL Database',
        height: 300,
        store: store,
        columns : userColumns,
        enableColumnHide: false,
        viewConfig: {
            forceFit: true
        }
    });
 
}
Ext.onReady(buildWindow);
</script>
</head>
<body>
 
<div class="container" style="width:500px">
    <div id="form-first"></div>
    <div id="user-grid"></div>
</div>
 
</body>
</html>
23
Apr
11

CRUD with Extjs

The content of this page has moved/combined with another page: https://numberformat.wordpress.com/2011/04/07/restful-crud-using-extjs/

07
Apr
11

RESTful CRUD using extJS

This page describes how to create an extJS based web application that performs RESTful CRUD operations on resources in a Java JEE web application. For those that want instant gratification, a complete working version of the project is available at the following SVN location. http://ext-jsf.googlecode.com/svn/trunk/wordpress/2011/04/restful-crud-using-extjs/. Get the project and skip to the Test The Application section of this page.

Background

Each Entity object (resource) suppose to have a unique URL in a REST based application. This design consideration makes it easy for JavaScript frameworks like extJS to perform CRUD operations on the resource based on user’s actions.

This tutorial builds from an article about ExtJS + Spring MVC validation using commons-validator. While that page only allowed you to create new items by issuing a HTTP POST, this page will complete the CRUD concept allowing the browser to issue GET, PUT, and DELETE methods to manage items in the database.

CRUD is composed of the following parts:

Create(aka. HTTP POST)

The extJS component issues a HTTP POST to the URL listed in the data store. HTTP POST is neither safe or omnipotent. This means that the HTTP POST can change the state of the object and also multiple invocations can be disruptive (it will create an extra object if called twice by mistake). The component is expecting a JSON “success” string and a representation of the object at a minimum. The id of the item is not passed in the URL because it is not known yet.

Read (aka. HTTP GET)

The extJS component is expecting a list of objects to be returned when it issues a GET to the URL. The list of items are displayed on the grid based on the configuration of the reader or metaData. HTTP GET is a “safe” operation. This means that calls to a resource using GET will not change the state of the resource. The component is expecting a JSON “success” string at a minimum.

Update (aka. HTTP PUT)

The extJS component sends the id in the URL path and the representation of the modified object in the body of the PUT. HTTP PUT is not “safe” however it is “omnipotent”. This means that an HTTP PUT can change the state of the object however multiple submissions of this PUT will not will not be disruptive. The component is expecting a JSON “success” and a representation of the object at a minimum.

Destroy (aka. HTTP DELETE)

The extJS component sends the id in the URL path. HTTP DELETE is not safe however it is omnipotent. This means that DELETE operations will change the state of the object on the server side however multiple invocations of this will not be disruptive. (since deleting the same id the second time will not do anything). The component is expecting a JSON “success” string at a minimum.

This page describes all you need to create and configure a self-contained java application that interacts with an in-memory Java database.

Requirements

Procedure

Verify The Existing Project

Open up a console with to the project’s base directory.

Convert XML to POJO

The Grid on this page uses a XmlWriter to submit the Item as XML in the Request Body. In order to convert the XML into a JavaBean we will be using JAXB.

Converting XML to JavaBeans using JAXB was covered on the following page “using jaxb to convert between xml and pojo“.

Modify Item.java and define “@XmlRootElement” right above the class name and add the extra import statement.

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

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

@XmlRootElement
public class Item {
...

The following class is used by JAXB to create an instance of an Item Bean.

src/main/java/com/test/ObjectFactory.java

package com.test;

import javax.xml.bind.annotation.XmlRegistry;

@XmlRegistry
public class ObjectFactory {
 
    public ObjectFactory() {
    }
 
    public Item createItem() {
        return new Item();
    }
}

Binding the Bean from the Request Body

The request body contains XML representation of beans. Spring MVC 2.5 has a @ModelAttribute annotation that does this for us. However the default implementation expects the input as HTTP form attributes. For example (“itemId=1&name=testName&description=TestDescription”). Since the Grid will be posting the Bean as XML we need to override a few classes.

The following 4 classes need to be created:

  1. CustomAnnotationMethodHandlerAdapter.java
  2. XMLServletRequestDataBinder.java
  3. ServletRequestBodyXMLPropertyValues.java
  4. XMLWebUtils.java

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

package com.test.fw;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;

/**
 * The whole purpose of this class it to allow it to specify XMLServletRequestDataBinder
 * instead of the ServletRequestDataBinder as defined in the parent class.
 */
public class CustomAnnotationMethodHandlerAdapter extends AnnotationMethodHandlerAdapter {
	@Override
	protected ServletRequestDataBinder createBinder(HttpServletRequest request,
			Object target, String objectName) throws Exception {		
		return new XMLServletRequestDataBinder(target, objectName);
	}
}

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

package com.test.fw;

import javax.servlet.ServletRequest;

import org.springframework.beans.MutablePropertyValues;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.multipart.MultipartHttpServletRequest;

/**
 * The purpose of this class it call ServletRequestBodyXMLPropertyValues instead
 * of ServletRequestPropertyValues in the bind method.
 */
public class XMLServletRequestDataBinder extends ServletRequestDataBinder {

	public XMLServletRequestDataBinder(Object target, String objectName) {
		super(target, objectName);
	}

	@Override
	public void bind(ServletRequest request) {
		MutablePropertyValues mpvs = new ServletRequestBodyXMLPropertyValues(request);
		if (request instanceof MultipartHttpServletRequest) {
			MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
			bindMultipartFiles(multipartRequest.getFileMap(), mpvs);
		}
		doBind(mpvs);
	}

}

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

package com.test.fw;

import javax.servlet.ServletRequest;

import org.springframework.beans.MutablePropertyValues;


/**
 * PropertyValues implementation created from XML in the ServletRequest body. This 
 * implementation uses the XMLWebUtils instead of WebUtils to extract property values
 * from the ServletRequest body.
 */
public class ServletRequestBodyXMLPropertyValues extends MutablePropertyValues {
	private static final long serialVersionUID = 1L;

	public ServletRequestBodyXMLPropertyValues(ServletRequest request) {
		this(request, null, null);
	}
	
	public ServletRequestBodyXMLPropertyValues(ServletRequest request, String prefix, String prefixSeparator) {
		super(XMLWebUtils.getElementsStartingWith(
				request, (prefix != null) ? prefix + prefixSeparator : null));
	}

}

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

package com.test.fw;

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

import javax.servlet.ServletRequest;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.springframework.util.Assert;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * This class offers some enhanced features relating to REST processing
 * XML submitted by extJS component as part of a form submission.
 */
public class XMLWebUtils {
	static class MyDefaultHandler extends DefaultHandler {
		private String tempVal;
		
		final private Map<String, String> params;
		private String rootElement;
		private String prefix;
		
		public MyDefaultHandler(Map<String, String> map, String prefix) {
			this.params = map;
			this.prefix = prefix;
		}
		
		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) throws SAXException {
			if(qName == null) {
				return;
			}
			
			if(rootElement == null) {
				rootElement = qName;
			} 
			
		}
	    
		public void characters(char[] ch, int start, int length) throws SAXException {
	        tempVal = new String(ch,start,length);
	    }
	    
		@Override
		public void endElement(String uri, String localName, String qName)
				throws SAXException {

			if(qName.equalsIgnoreCase(rootElement)) {
				rootElement = null;
			} else {
				params.put(qName, tempVal);
				tempVal = null;
			}
		}
	}
	
	/**
	 * This method creates a map of element values submitted by the extJS component.
	 * The extjs component has been set to submit an xml representation of the data. 
	 * This method uses SAX API that is built into the JDK 1.5 and above.
	 * 
	 * @param request
	 * @param prefix
	 * @return
	 */
	public static Map<String, String> getElementsStartingWith(ServletRequest request, String prefix) {
		Assert.notNull(request, "Request must not be null");
		
		Map<String, String> params = new TreeMap<String, String>();

		if (prefix == null) {
			prefix = "";
		}
        //get a factory
        SAXParserFactory spf = SAXParserFactory.newInstance();
        try {
            SAXParser sp = spf.newSAXParser();
            sp.parse(request.getInputStream(), new MyDefaultHandler(params, prefix));
        }catch(SAXException se) {
            se.printStackTrace();
        }catch(ParserConfigurationException pce) {
            pce.printStackTrace();
        }catch (IOException ie) {
            ie.printStackTrace();
        }
		return params;
	}
}

Configure SpringMVC to use the Custom Annotation Method Handler Adapter

The AnnotationMethodHandlerAdapters is configured using the spring-servlet.xml file. You can only have one HandlerAdapter per URL mapping. We will configure an alternate HandlerAdapter using another mapping.

Modify the web.xml and add an additional DispatcherServlet.

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

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    id="WebApp_ID" version="2.5">
 
    <display-name>Archetype Created Web Application</display-name>
 
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
 
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
 
    <listener>
        <listener-class>com.test.DBLifecycleContextListener</listener-class>
    </listener>
 
    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>springdata</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>springdata</servlet-name>
        <url-pattern>/appdata/*</url-pattern>
    </servlet-mapping>
 
</web-app>

The following is the configuration file for the new DispatcherServlet we defined above. Spring MVC follows a standard naming convention with reading xml configurations. If the servlet name is “springdata” then the springdata-servlet.xml file in the WEB-INF folder will be read. The below configuration file is similar to the original one except we have swapped out the HandlerAdapter for “com.test.CustomAnnotationMethodHandlerAdapter”.

src/main/webapp/WEB-INF/springdata-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
 
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
 
http://www.springframework.org/schema/context
 
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
 
    <context:annotation-config />
    <context:component-scan base-package="com.test" />
 
    <bean
        class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
    <bean
        class="com.test.fw.CustomAnnotationMethodHandlerAdapter" />
 
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>
 
</beans>

Setup the ExtJS Front End JavaScript/HTML

Lastly we will set-up the extJS Front End.

The following enhances the extJS editor grid to display validation failures. Thanks to Oskar Johansson for providing a excellent solution.

src/main/webapp/gridValidation.js

// Oskar Johansson, http://onkelborg.com
    activateGridValidation = function (grid) {
        var store = grid.getStore();
        var mapRecordToRequestJsonData = [];
        var mapRecordToRequestRecord = [];
        var storeOnBeforeSave = function (store, data) {
            mapRecordToRequestJsonData = [];
            mapRecordToRequestRecord = [];
        };
        var storeOnBeforeWrite = function (dataProxy, action, rs, params) {
            mapRecordToRequestJsonData.push(params.jsonData);
            mapRecordToRequestRecord.push(rs);
        };
        var storeOnException = function (dataProxy, type, action, options, response, arg) {
            if (action == "create" || action == "update") {
                var erroneousObject = mapRecordToRequestRecord[mapRecordToRequestJsonData.indexOf(options.jsonData)];
                var rowIndex = store.indexOf(erroneousObject);
                var colModel = grid.getColumnModel();
                var gridView = grid.getView();
                var errorsInner;
                var errors = response.raw ? response.raw.errors : ((errorsInner = Ext.decode(response.responseText)) ? errorsInner.errors : null);
                var editableColumns = [];
                for (var i = 0; i < colModel.getColumnCount(); i++) {
                    var column = colModel.getColumnById(colModel.getColumnId(i));
                    if (column.getCellEditor(rowIndex)) {
                        editableColumns.push(colModel.getDataIndex(i));
                    }
                }
                if (errors) {
                    var erroneousColumns = [];
                    for (var x in errors) {
                        erroneousColumns.push(x);
                    }
                    for (var i = 0; i < erroneousColumns.length; i++) {
                        editableColumns.splice(editableColumns.indexOf(erroneousColumns[i]), 1);
                    }
                    for (var i = 0; i < erroneousColumns.length; i++) {
                        var errKey = erroneousColumns[i];
                        var colIndex = colModel.findColumnIndex(errKey);
                        var msg = errors[errKey];
                        var cell = gridView.getCell(rowIndex, colIndex);
                        var cellElement = Ext.get(cell);
                        var cellInnerElement = cellElement.first();
                        cellInnerElement.addClass("x-form-invalid");
                        cellInnerElement.set({ qtip: msg });
                    }
                }
                for (var i = 0; i < editableColumns.length; i++) {
                    var colIndex = colModel.findColumnIndex(editableColumns[i]);
                    var cell = gridView.getCell(rowIndex, colIndex);
                    var cellElement = Ext.get(cell);
                    var cellInnerElement = cellElement.first();
                    cellInnerElement.removeClass("x-form-invalid");
                    cellInnerElement.set({ qtip: "" });
                }
            }
        };
        store.proxy.on("exception", storeOnException);
        store.proxy.on("beforewrite", storeOnBeforeWrite);
        store.on("beforesave", storeOnBeforeSave);
        grid.on("destroy", function () {
            store.proxy.un("exception", storeOnException);
            store.proxy.un("beforewrite", storeOnBeforeWrite);
            store.un("beforesave", storeOnBeforeSave);
        });
    }; 

The next file contains the JavaScript code that sets up the editor grid panel.
src/main/webapp/restful.js

MyApp={};
MyApp.getContext = function() {
	var base = document.getElementsByTagName('base')[0];
	if (base && base.href && (base.href.length > 0)) {
		base = base.href;
	} else {
		base = document.URL;
	}
	return base.substr(0, base.indexOf("/", base.indexOf("/", base.indexOf("//") + 2) + 1));
};

var App = new Ext.App({});
 
var proxy = new Ext.data.HttpProxy({
	url: MyApp.getContext() + '/appdata/itemresource'
});

var reader = new Ext.data.JsonReader({
    totalProperty: 'total',
    successProperty: 'success',
    idProperty: 'itemId',
    root: 'data',
    record: 'item',
    messageProperty: 'message',  // <-- New "messageProperty" meta-data
    fields : [
              {name: 'itemId'},
              {name: 'name', allowBlank: false},
              {name: 'description', allowBlank: true},
              {name: 'stockQty', allowBlank: true},
              {name: 'color', allowBlank: true}
    ]
});

// The new DataWriter component.
var writer = new Ext.data.XmlWriter({
    encode : false,
    writeAllFields : true,
    xmlEncoding : "UTF-8"
});

// Typical Store collecting the Proxy, Reader and Writer together.
var store = new Ext.data.Store({
    id: 'item',
    restful: true,     // <-- This Store is RESTful
    proxy: proxy,
    reader: reader,
    writer: writer    // <-- plug a DataWriter into the store
});

// load the store
store.load();

// The following is necessary to map the Record Fields to the grid columns.
var userColumns =  [
    {header: "Item Id", width: 40, sortable: true, dataIndex: 'itemId'},
    {header: "Item Name", width: 100, sortable: true, dataIndex: 'name', editor: new Ext.form.TextField({})},
    {header: "Description", width: 100, sortable: true, dataIndex: 'description', editor: new Ext.form.TextField({})},
    {header: "Quantity", width: 100, sortable: true, dataIndex: 'stockQty', editor: new Ext.form.TextField({})},
    {header: "Color", width: 100, sortable: true, dataIndex: 'color', editor: new Ext.form.TextField({})}
];

Ext.onReady(function() {
    Ext.QuickTips.init();

    // use RowEditor for editing
    var editor = new Ext.ux.grid.RowEditor({
        saveText: 'Save'
    });

    // Create a typical GridPanel with RowEditor plugin
    var userGrid = new Ext.grid.GridPanel({
        renderTo: 'user-grid',
        iconCls: 'icon-grid',
        frame: true,
        title: 'Fun with RESTful CRUD',
        height: 300,
        store: store,
        plugins: [editor],
        columns : userColumns,
    	enableColumnHide: false,
        tbar: [{
        	id: 'addBtn',
            text: 'Add',
            iconCls: 'silk-add',
            handler: onAdd
        }, '-', {
        	id: 'deleteBtn',
            text: 'Delete',
            iconCls: 'silk-delete',
            handler: onDelete
        }, '-'],
        viewConfig: {
            forceFit: true
        }
    });

    function onAdd(btn, ev) {
        var u = new userGrid.store.recordType({
            first : '',
            last: '',
            email : ''
        });
        editor.stopEditing();
        userGrid.store.insert(0, u);
        editor.startEditing(0);
    }

    // Disable the buttons when editing.
    editor.addListener('beforeedit', function(roweditor, rowIndex){
        Ext.getCmp('addBtn').disable();
        Ext.getCmp('deleteBtn').disable();
    });
    editor.addListener('afteredit', function(roweditor, rowIndex){
        Ext.getCmp('addBtn').enable();
        Ext.getCmp('deleteBtn').enable();
    });
    editor.addListener('canceledit', function(roweditor, rowIndex){
    	// get fresh records from the database.
        userGrid.store.reload({});
        Ext.getCmp('addBtn').enable();
        Ext.getCmp('deleteBtn').enable();
    });     

    function onDelete() {
        var rec = userGrid.getSelectionModel().getSelected();
        if (!rec) {
            return false;
        }
        userGrid.store.remove(rec);
    }
    activateGridValidation(userGrid);
});

The following file pulls everything together.
src/main/webapp/grid.html

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<title>RESTful Store Example</title>

<!-- extJS JavaScript files -->
<script type="text/javascript"
    src="http://dev.sencha.com/deploy/ext-3.3.1/adapter/ext/ext-base-debug.js">
</script> 
<script type="text/javascript"
    src="http://dev.sencha.com/deploy/ext-3.3.1/ext-all-debug.js">
</script>
<!-- extJS example JavaScript files -->
<script type="text/javascript"
	src="http://dev.sencha.com/deploy/ext-3.3.1/examples/shared/extjs/App.js"></script>
<script type="text/javascript"
	src="http://dev.sencha.com/deploy/ext-3.3.1/examples/ux/RowEditor.js"></script>

<!-- My JavaScript Files -->
<script type="text/javascript" src="restful.js"></script>
<script type="text/javascript" src="gridValidation.js"></script>
 
<!-- Common Styles -->
<link rel="stylesheet" type="text/css"
    href="http://dev.sencha.com/deploy/ext-3.3.1/resources/css/ext-all.css"/>
<link rel="stylesheet" type="text/css" href="restful.css" />
<!-- Example Styles -->
<link rel="stylesheet"
	href="http://dev.sencha.com/deploy/ext-3.3.1/examples/ux/css/RowEditor.css" />
<link rel="stylesheet" type="text/css"
	href="http://dev.sencha.com/deploy/ext-3.3.1/examples/shared/examples.css" />
<link rel="stylesheet" type="text/css"
	href="http://dev.sencha.com/deploy/ext-3.3.1/examples/shared/icons/silk.css" />

</head>
<body>
<h1>RESTful Editor Grid with Validation</h1>
<h3>About This Example:</h3>
    <div><ul>
    <li>Uses HyperSQL, IBATIS, Spring</li>
    </ul></div>

<div class="container" style="width:500px">
    <div id="user-grid"></div>
</div>
<h3>Tips:</h3>
    <div><ul>
    <li>1. Use Firebug to inspect RESTful operations</li>
    <li>2. Validation rules are modified by editing src/main/webapp/WEB-INF/validation.xml</li>
    </ul></div>

<h3>Steps to Add a new Column:</h3>
    <div><ul>
    <li>1. Add column to the database src/main/resources/ddl.xml</li>
    <li>2. Add column to the grid located in restful.js</li>
    <li>3. Add the new attribute to Item.java Bean (create getters/setters as well)</li>
    <li>4. Change IBATIS config file located in src/main/resources/com/test/ItemDataManager.xml</li>
    <li>5. Optionally Add column validation to src/main/webapp/WEB-INF/validation.xml</li>
    </ul></div>

</body>
</html>

Modify the index.html file and add the link to the above page.

The index.html should look like this.

src/main/webapp/index.html

<html>
<body>
<h3><a href="http://localhost:8080/crud/app/item">Part I - Simple form.</a></h3>
<h3><a href="form.html">Part II - Click here for the ExtJS Form</a></h3>
<h3><a href="grid.html">Part III -Click here for the ExtJS Editor Grid</a></h3>
</body>
</html>

Start Jetty Servlet Engine

mvn clean compile jetty:run

Test the application

Navigate to: http://localhost:8080/crud/ and enter some data

Test the following as well.
1. then shutting down jetty – CTRL-C
2. verify the data survived in the src/main/webapp/WEB-INF/db directory.

Create a WAR file

If you want to run the application in another environment you can create a war file and drop it into a server of your choice. This method will also package up any runtime dependencies and put them in the /WEB-INF/lib folder of the WAR file.

mvn clean compile package

The war file should appear in the target/ folder.

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

28
Mar
11

Using Spring to perform CRUD on an HSQLDB using Ibatis

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

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

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

Background

CRUD = Create Read Update Delete.

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

Requirements

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

Procedure

Outline

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

Create a new project

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

groupId: com.test
artifactId: crud

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

cd to the project base folder.

cd crud

Since this is a web project maven2 does not create the java source folder.

Create missing folders now.
mkdir -p src/main/java/com/test
mkdir -p src/main/resources/com/test
mkdir -p src/main/webapp/WEB-INF/jsp

Modify the project Configuration

The pom.xml file should look something like this.

vi pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.test</groupId>
    <artifactId>crud</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>crud Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <pluginRepositories>
      <pluginRepository>
        <id>numberformat-releases</id>
        <url>https://raw.github.com/numberformat/20130213/master/repo</url>
      </pluginRepository>
    </pluginRepositories>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>20090211</version>
        </dependency>
        <dependency>
            <groupId>org.apache.ibatis</groupId>
            <artifactId>ibatis-sqlmap</artifactId>
            <version>2.3.4.726</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring</artifactId>
            <version>2.5.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>2.5.6</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.14</version>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>1.8.0.10</version>
        </dependency>
        <dependency>
            <groupId>org.apache.ddlutils</groupId>
            <artifactId>ddlutils</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <finalName>crud</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>7.0.0.v20091005</version>
                <configuration>
                    <scanIntervalSeconds>2</scanIntervalSeconds>
                    <webAppConfig>
                        <contextPath>/crud</contextPath>
                    </webAppConfig>
                    <stopKey>s</stopKey>
                </configuration>
            </plugin>
            <plugin>
                <groupId>github.numberformat</groupId>
                <artifactId>blog-plugin</artifactId>
                <version>1.0-SNAPSHOT</version>
                <configuration>
                <gitUrl>https://github.com/numberformat/20110328</gitUrl>
                </configuration>
            <executions>
              <execution>
                <id>1</id>
                <phase>site</phase>
                <goals>
                  <goal>generate</goal>
                </goals>
              </execution>
            </executions>
            </plugin>
        </plugins>

    </build>
</project>

Database Setup and Configuration

The following configuration file is used by Apache DDLUtils to generate apply the schema changes each time the application restarts. Changes are made incrementally and every attempt is made to preserve the data. For more information about DDLUtils please see my other tutorial.

vi src/main/resources/ddl.xml

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

<database name="testdb">

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

</database>

Since we are working with HSQLDB, it is critical that we gracefully shut-down the database when the application stops. Otherwise the data that was cached in memory will be lost.

We will use a “ServletContextListener” to listen for the application shut-down event. This will allow us to execute code to graceful shut-down the HSQLDB database.

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

package com.test;

import java.io.InputStreamReader;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.sql.DataSource;

import org.apache.ddlutils.Platform;
import org.apache.ddlutils.PlatformFactory;
import org.apache.ddlutils.io.DatabaseIO;
import org.apache.ddlutils.model.Database;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 * This listener initializes or gracefully shuts down the database based on
 * events from the web application.
 */
public class DBLifecycleContextListener implements ServletContextListener {
    private DataSource dataSource = null;
    private WebApplicationContext springContext = null;

    public void contextDestroyed(ServletContextEvent event) {
        SimpleJdbcTemplate template = new SimpleJdbcTemplate(dataSource);
        template.update("SHUTDOWN;");
        System.out.println("Database Successfully Shutdown.");
    }

    public void contextInitialized(ServletContextEvent event) {
        springContext = WebApplicationContextUtils
                .getWebApplicationContext(event.getServletContext());
        dataSource = (DataSource) springContext.getBean("dataSource");

        Platform platform = PlatformFactory
                .createNewPlatformInstance(dataSource);

        Database database = new DatabaseIO().read(new InputStreamReader(
                getClass().getResourceAsStream("/ddl.xml")));

        platform.alterTables(database, false);
    }
}

Spring Application

The Spring application consists of a

  • Item – POJO that represents the individual item
  • ItemModel – Service Interface
  • ItemModelImpl – Implementation of the Service
  • ItemDataManager – DataManager Interface
  • ItemDataManagerImpl – DataManager Implementation

The majority of the configuration is Annotation Based. For more information on annotation based spring configuration please see my other tutorial “using annotations in the spring framework“. Also checkout the following: “hello world spring mvc with annotations“.

Item Bean

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

package com.test;

import java.math.BigDecimal;

/**
 * Represents an item in inventory or in a shopping cart.
 */
public class Item {
    private Integer itemId;
    private String oemId;
    private String name;
    private String description;
    private String imageURL;
    private String color;
    private BigDecimal price;
    private BigDecimal length;
    private BigDecimal width;
    private BigDecimal height;

    /**
     * Weight in grams
     */
    private BigDecimal weight;
    /**
     * Quantity in stock.
     */
    private Integer stockQty;

    public Item() {
        super();
    }

    public Item(Integer id, String name) {
        this.name = name;
        this.itemId = id;
    }

    public Integer getItemId() {
        return itemId;
    }
    public void setItemId(Integer itemId) {
        this.itemId = itemId;
    }
    public String getOemId() {
        return oemId;
    }
    public void setOemId(String oemId) {
        this.oemId = oemId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public String getImageURL() {
        return imageURL;
    }
    public void setImageURL(String imageURL) {
        this.imageURL = imageURL;
    }
    public BigDecimal getPrice() {
        return price;
    }
    public void setPrice(BigDecimal price) {
        this.price = price;
    }
    public BigDecimal getLength() {
        return length;
    }
    public void setLength(BigDecimal length) {
        this.length = length;
    }
    public BigDecimal getWidth() {
        return width;
    }
    public void setWidth(BigDecimal width) {
        this.width = width;
    }
    public BigDecimal getHeight() {
        return height;
    }
    public void setHeight(BigDecimal height) {
        this.height = height;
    }
    public BigDecimal getWeight() {
        return weight;
    }
    public void setWeight(BigDecimal weight) {
        this.weight = weight;
    }

    public Integer getStockQty() {
        return stockQty;
    }

    public void setStockQty(Integer stockQty) {
        this.stockQty = stockQty;
    }

}

Spring Model classes

The following interface and implementation classes are currently being used a pass-thru to the data manager. Its not adding any value to the application but once we start using Spring Transactions we will be glad we did it this way.

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

package com.test;

import java.util.List;

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

    public Item createItem(Item item);

    public void updateItem(Item item);

    public void deleteItem(Integer itemId);

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

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

    public void deleteItemImage(Integer itemId);

}

The following is the implementation class.

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

package com.test;

import java.util.List;

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

@Service
public class ItemModelImpl implements ItemModel {

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

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

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

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

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

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

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

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

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

    public ItemDataManager getItemDataManager() {
        return itemDataManager;
    }

}

Spring Data Managers

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

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

package com.test;

import java.util.List;

public interface ItemDataManager {

    public Item getItemInfo(Integer itemId);

    public Item createItem(Item item);

    public void updateItem(Item item);

    public void deleteItem(Integer itemId);

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

    int getItemCount();

    public void deleteItemImage(Integer itemId);
}

The following is the data manager implementation class.

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

package com.test;

import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.orm.ibatis.SqlMapClientTemplate;
import org.springframework.stereotype.Repository;

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

    public SqlMapClientTemplate getSqlMapClientTemplate() {
        return sqlMapClientTemplate;
    }

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

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

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

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

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

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

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

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

        return list;
    }

}

Spring Configuration

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

vi src/main/resources/applicationContext.xml

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

http://www.springframework.org/schema/beans/spring-beans-2.5.xsd

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

http://www.springframework.org/schema/context/spring-context-2.5.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.test"/>

    <bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:file:src/main/webapp/WEB-INF/db/testdb"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>

<!-- Used for Ibatis -->
    <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="configLocation" value="/WEB-INF/mapper-config.xml"/>
        <property name="dataSource" ref="dataSource"/>
        <property name="useTransactionAwareDataSource" value="true"/>
    </bean>
    <bean id="sqlMapClientTemplate" class="org.springframework.orm.ibatis.SqlMapClientTemplate">
        <property name="sqlMapClient" ref="sqlMapClient"/>
    </bean>

</beans>

Configure simple logging to the console.

vi src/main/resources/log4j.properties

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

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

Integration with IBATIS

The following needs to be defined in the spring configuration. The SQLMapClientTemplate is the primary class that will be used by our data-mangers to query the database thru SQL statements and stored procedure calls. It needs a reference to sqlMapClient that we just created.

The following is the ibatis main configuration file. Since we are using spring framework there is not much configuration that you need to put in here. All that is required is that ibatis knows where the xml files for all the Data Managers are located.

For more information on ibatis please see my other tutorial “ibatis 2.3 spring 2.5 hello world

vi src/main/webapp/WEB-INF/mapper-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
        PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
        "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
    <sqlMap resource="com/test/ItemDataManager.xml"/>
</sqlMapConfig>

vi src/main/resources/com/test/ItemDataManager.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
        PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
        "http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="Item">

<typeAlias alias="Item" type="com.test.Item" />

    <select id="allItems" resultClass="Item">
        select item_id as itemId,
        oem_id as oemId,
        item_x as name,
        description as description,
        color as color,
        image_url as imageURL,
        price as price,
        length as length,
        width as width,
        height as height,
        weight as weight,
        stock_qty as stockQty
        from item;
    </select>
    <select id="getItemInfo" parameterClass="int" resultClass="Item">
        select item_id as itemId,
        oem_id as oemId,
        item_x as name,
        description as description,
        color as color,
        image_url as imageURL,
        price as price,
        length as length,
        width as width,
        height as height,
        weight as weight,
        stock_qty as stockQty
        from item where item_id = #id#;
    </select>

    <insert id="itemInsert" parameterClass="Item">
        insert into item (oem_id,
            item_x,
            description,
            color,
            image_url,
            price,
            length,
            width,
            height,
            weight,
            stock_qty)
        values ( #oemId#, #name#, #description#, #color#,
            #imageURL#, #price#, #length#, #width#,
            #height#, #weight#, #stockQty# );
        <selectKey resultClass="int" >
            <!-- HSQLDB -->
            CALL IDENTITY();
            <!-- MySQL -->
            <!-- SELECT LAST_INSERT_ID(); -->
        </selectKey>
    </insert>
    <update id="updateItem" parameterClass="Item">
        update item set oem_id = #oemId#, item_x = #name#, description=#description#,
            color = #color#, image_url = #imageURL#, price = #price#, length = #length#,
            width = #width#, height = #height#, weight = #weight#, stock_qty = #stockQty#
        where item_id = #itemId#;
    </update>
    <delete id="deleteItem" parameterClass="java.lang.Integer">
        delete from item where item_id = #itemId#;
    </delete>
</sqlMap>

Tying it all Together with Spring MVC

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

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

package com.test;

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

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

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

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

    private ItemModel itemModel;

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

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

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

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

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

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

    public ItemModel getItemModel() {
        return itemModel;
    }

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

}

Create the Test HTML Form

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

vi src/main/webapp/WEB-INF/jsp/index.jsp

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>DB Parameters</title>
<meta http-equiv="Content-type" content="text/html; charset=iso-8859-1" />
<meta http-equiv="Content-Language" content="en-us" />

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

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

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

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

Web Application Configuration

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

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

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

    <display-name>Archetype Created Web Application</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <listener>
        <listener-class>com.test.DBLifecycleContextListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>

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

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

http://www.springframework.org/schema/beans/spring-beans-2.5.xsd

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

http://www.springframework.org/schema/context/spring-context-2.5.xsd">

    <context:annotation-config />
    <context:component-scan base-package="com.test" />

    <bean
        class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
    <bean
        class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />

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

</beans>

Start Jetty Servlet Engine

mvn clean compile jetty:run

Test the application

 

 

Test the following as well.

  1. then shutting down jetty – CTRL-C
  2. verify the data survived in the src/main/webapp/WEB-INF/db directory.

Creating a WAR file

If you want to run the application in another environment you can create a war file and drop it into a server of your choice. This method will also package up any runtime dependencies and put them in the /WEB-INF/lib folder of the WAR file.

mvn clean compile package

The war file should appear in the following location…

target/crud-1.0-SNAPSHOT.war

Next Steps

Full downloadable source for this page is available here.
02
Jan
10

User Interactions with JSF Forms

This page is about JSF Managed Beans and Forms. Its about how to break up the form submission process to handle large amount of fields. We will also cover how to avoid having a Flat Bean with only String based attributes. Instead we will create create a domain rich bean that will be easy to understand and maintain in the long run. We will cover techniques of creating/reading/updating/deleting these sub-beans. We will also cover a technique of cleaning up session beans once they are no longer needed.

  • Accept and validate large amount of data in multiple steps wizard approach.
  • Managed Beans and how handle sub-entities
  • Cleaning up session after completing a workflow

Managed Beans

JSF Managed beans hold data that is submitted by the user. Each field is an attribute of the managed bean. The submit buttons on the form can execute methods on the managed bean. The methods don’t need any arguments since the managed bean already contains the input elements of the form. Managed beans are not shared between users of the web application. Managed Beans have different types of scopes but the request and session scope are the most popular.

Large Forms

JSF forms are represented by Managed Beans. Sometimes forms get quite large and should be broken up into smaller chunks. Forms can be broken up into multiple pages by setting the scope to session and just having the pages bind to only a part of the Managed Bean. Provide “previous” and “next” buttons for user navigation. The “previous” buttons should not validate the form. You can do this by specifying immediate=’true’ in the command button. In general if you don’t want the page to get validated you can specify this attribute. Since validation is handled on a page by page basis you don’t need to worry about the step number like you did in the struts 1 framework.

Adding Sub-Beans to the Managed Bean

Another method of dealing with large forms is to break the backing bean into smaller chunks. This is where sub-beans and the Java Collections framework comes in handy. A typical case for this is when you have a bean that holds the user’s entire annual tax return. There is an area in the form where you specify zero one or many dependants. This is a good opportunity to use a Dependant Bean to hold each dependant and have a dependentList that is responsible for holding a list of them. All this can be maintained by the main tax Return Managed Bean. (see below)

	private Dependant dependant = new Dependant();
	private List<Dependant> dependantList = Dependant.getSample();

As you can see above we have a dependant, and a dependantList. One holds the information that was submitted by the user and the other holds the collection of dependents that was submitted so far.

One of the effective methods of allowing the user to add sub-beans is by presenting the user with a table and an add button. The table would contain a summary of already present sub-beans and allow the user to add more. At this point there are two options. 1) present the user with another page and have them enter the information or 2) have them enter the information on a popup window or similar. Once the user submits, validate and add the information to the main Bean. In this model the sub-bean plays the role of the data structure. None of the action methods are executed on the sub-bean.

When the user clicks add we do the following:

public String addDependant() {
	System.out.println("Adding dependant to list.");
	dependantList.add(dependant);
	// assign a new dependent object for the next submission.
	dependant = new Dependant();
	return "success";
}

The highlighted line is necessary since we are adding a reference to the user’s submitted form we don’t want the form that is subitted after this one to have the same reference. Hence we instantiate a new Dependent object for the next form submission.

Reading / Editing / Deleting Sub Entities

The system needs to know what the user clicked. The HtmlDataTable class is an extension to the dataTable component that the user is interacting with. When the user clicks a link, the component knows what data item the link was clicked for. The action handling method of the main bean needs to get the object and inspect it for further processing. In order for all this to work we need to “bind” the dataTable with the HTMLDataTable class in the managed Bean.

Binding to HTMLDataTable

The “binding” attribute is used to bind the dataTable to the HtmlDataTable attribute of the Managed Bean.

<h:dataTable border="1" value="#{taxReturn.dependantList}"  var="dependantrow" binding="#{taxReturn.dependantTable}">
<!--jsf column elements go here.-->
</h:dataTable>

In the TaxReturn has the following defined as an attribute.

private HtmlDataTable dependantTable;

Action Column

<h:column id="action_column">
<f:facet name="header"><h:outputText id="text6" value="action"></h:outputText></f:facet>
<h:commandLink id="remove" action="#{taxReturn.removeDependant}" immediate="true">
<h:outputText value="remove"></h:outputText>
</h:commandLink>
</h:column>

In the following code example shows the Dependant Bean being removed from a list of Dependant Objects.

public String removeDependant() {
	System.out.println("Removing dependant");
	Dependant dependantToRemove = (Dependant) dependantTable.getRowData();
	dependantList.remove(dependantToRemove);
	return "refresh";
}

Note: it is important to implement the equals() and hashCode() in order for this to work. Since the “business” key is used to identify if the objects are equal we don’t need to evaluate each and every field to determine equality. See my other page on implementing equals and hashcode.
Here is the equals and hashcode implementation.

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((ssn == null) ? 0 : ssn.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Dependant other = (Dependant) obj;
		if (ssn == null) {
			if (other.ssn != null)
				return false;
		} else if (!ssn.equals(other.ssn))
			return false;
		return true;
	}

JSF Managed Bean Session Cleanup

Once a JSF workflow is complete and the managed bean is in the session it should be cleaned up. Otherwise the object will stay around until the session expires. On a high volume website this can take up precious memory. To cleanup the session the Managed bean should execute the following.

public String processAndCleanup() {
	ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
	Map map = externalContext.getSessionMap();
	map.remove("taxReturn"); // the name of the jsf managed bean.
	return "success";
}

The only drawback with this approach I see, and it might not be a big deal is that the Managed Bean is aware of the ExternalContext which is a JSF artifact. If you have a better way to do this please comment below.

Snoop Servlet

During development you may verify that the bean is out of the session by viewing the output of the SnoopServlet.

Thats all for now!

27
Oct
09

Ibatis 2.3 spring 2.5 Hello World

This page describes a very simple HelloWorld type application being created so that it can be used as a base for more complex projects.

We start out with adding this to an already created pom.xml file.

                <dependency>
                        <groupId>org.apache.ibatis</groupId>
                        <artifactId>ibatis-sqlmap</artifactId>
                        <version>2.3.4.726</version>
                </dependency>

If you have not done so already add the jdbc drivers for your database. In my case I am using mySQL.

                <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>5.1.9</version>
                </dependency>

regenerate the eclipse project:
mvn eclipse:clean eclipse:eclipse

REturn back to eclipse and refresh the project.

IBatis Main Configuration File

We first start with defining the main configuration file for the Ibatis framework. If you are working with a web application this is typically your WEB-INF folder. If you were using ibatis as a standalone framework you would have needed to put additional configuration entries in this file. However since we are using this with the state of-the-art dependency injection functionality provided by the spring framework.

MappersConfig.xml currently contains the locations of all the xml configuration files of all the individual sqlMap files.

MapperConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
        PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
        "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
    <sqlMap resource="com/vermatech/electronics/repository/ItemDataManager.xml"/>
</sqlMapConfig>

SQL Maps configuration files

Each SQL MAp resource specifies a set of sql statements that may be run against an entity. Inserts, updates, deletes etc are specified within XML syntax. In addition to SQL Statements you may specify additional attributes like how long you want the statements to cache. Aliases may be defined for class names. REsults map for returned results and parameter map for parameters.

Parameters Maps and Result Maps

Here is an example that uses parameters map and results maps explicitly

<typeAlias alias=▒??product▒?? type=▒??com.ibatis.example.Product▒?? />
<parameterMap id=▒??productParam▒?? class=▒??product▒??>
         <parameter property=▒??id▒??/>
</parameterMap>
<resultMap id=▒??productResult▒?? class=▒??product▒??>
         <result property=▒??id▒?? column=▒??PRD_ID▒??/>
         <result property=▒??description▒?? column=▒??PRD_DESCRIPTION▒??/>
</resultMap>
<select id=▒??getProduct▒?? parameterMap=▒??productParam▒??
         resultMap=▒??productResult▒?? cacheModel=▒??product-cache▒??>
    select * from PRODUCT where PRD_ID = ?
</select>

If you want to keep the xml configuration simple you can have Ibatis make educated guesses on how to map a Bean to Parameter and from Results back to beans. There is a slight performance consequence in that this approach requires accessing the ResultSetMetaData. This limitation can be overcome by using an explicit resultMap. Result maps are described in more detail later in this document. This uses the auto mapping feature of the framework which could be slower than if you specified the mappings yourself.

Here is an example that allows the sqlMap to make educated guesses about the mappings

<?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=▒??Product▒??>
   <select id=▒??getProduct▒?? parameterClass=▒?? com.ibatis.example.Product▒??
                              resultClass=▒??com.ibatis.example.Product▒??>
         select
            PRD_ID as id,
            PRD_DESCRIPTION as description
         from PRODUCT
         where PRD_ID = #id#
   </select>
</sqlMap>

Since the developer is specifying SQL witin XML you need the ability to escape characters that would appear in xml. An example is the < and > signs.

<select id="getPersonsByAge" parameterClass=▒??int▒?? resultClass="examples.domain.Person">
         SELECT *
         FROM PERSON
         WHERE AGE <![CDATA[ > ]]> #value#
</select>

SQL Fragments

In order to reduce SQL Code duplication you can use things like Fragments but keep in mind that it does increase the amount of testing effort if something in the common fragment changes.

Here is an example:

<sql id="selectItem_fragment">
         FROM items
         WHERE parentid = 6
</sql>
<select id="selectItemCount" resultClass="int">
         SELECT COUNT(*) AS total
         <include refid="selectItem_fragment"/>
</select>
<select id="selectItems" resultClass="Item">
         SELECT id, name
         <include refid="selectItem_fragment"/>
</select>

Auto generated keys

Supported by using the following XML code

<!▒??Oracle SEQUENCE Example -->
<insert id="insertProduct-ORACLE" parameterClass="com.domain.Product">
    <selectKey resultClass="int" >
         SELECT STOCKIDSEQUENCE.NEXTVAL AS ID FROM DUAL
    </selectKey>
    insert into PRODUCT (PRD_ID,PRD_DESCRIPTION)
    values (#id#,#description#)
</insert>
<!▒?? Microsoft SQL Server IDENTITY Column Example -->
<insert id="insertProduct-MS-SQL" parameterClass="com.domain.Product">
    insert into PRODUCT (PRD_DESCRIPTION)
    values (#description#)
    <selectKey resultClass="int" >
         SELECT @@IDENTITY AS ID
    </selectKey>
</insert>

Calling Stored Procedures

<parameterMap id="swapParameters" class="map" >
  <parameter property="email1" jdbcType="VARCHAR" javaType="java.lang.String" mode="INOUT"/>
  <parameter property="email2" jdbcType="VARCHAR" javaType="java.lang.String" mode="INOUT"/>
</parameterMap>
<procedure id="swapEmailAddresses" parameterMap="swapParameters" >
  {call swap_email_address (?, ?)}
</procedure>

The next steps are to start coding your CRUD operations on your objects. I talk about this in more detail at the following page

This site is a collaborative effort! The complete text and sourcecode for this is available on GitHub. Corrections and enhancements are welcome, please make the change and submit a pull request in the comment area below.



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

Join 77 other followers

September 2017
S M T W T F S
« Mar    
 12
3456789
10111213141516
17181920212223
24252627282930

Blog Stats

  • 830,676 hits