previous print
download
next
In step 2 a selection box for U.S. States was added to the simple address book. The collection of states was injected into AddressBookAction using Spring's dependency injection framework.
In this part of the tutorial, the object relational mapping tool, Hibernate, will be installed and configured in the address-book web application. Hibernate will provide the object-relational mapping service that will move an Address record to and from a relational database. The database used in this step is MySql.
Hibernate Annotations is used in this example which requires at least Hibernate 3.2 and Java 5. Adding Hibernate support to the address-book Struts-2 project requires the addition of the following libraries to the lib directory:
The Spring configuration file src/main/webapp/WEB-INF/applicationContext.xml is updated to include two bean definitions required by hibernate; the Data Source and Hibernate Session Factory. In the definition of the Session Factory the crud.model.Address bean is declared as an annotated class. Annotation will be discussed later in this article.
Note: In the URL of the data source there is a parameter createDatabaseIfNotExists=true. This will create a database called addressBook and a table called address in MySql if it does not already exist. The table does not need to exist prior to running the web application the first time.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans default-autowire="autodetect">
<bean id="statesList" class="crud.model.StatesList" />
<bean id="addressDao" class="crud.dao.AddressDaoHibernate" />
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName">
<value>com.mysql.jdbc.Driver</value>
</property>
<property name="url">
<value>
jdbc:mysql://localhost/AddressBook?createDatabaseIfNotExist=true
</value>
</property>
<property name="username">
<value>yourUsername</value>
</property>
<property name="password">
<value>yourPassword</value>
</property>
</bean>
<!-- Hibernate session factory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource">
<ref bean="dataSource"/>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect"> org.hibernate.dialect.MySQLDialect </prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
<property name="annotatedClasses">
<list>
<value>crud.model.Address</value>
</list>
</property>
</bean>
</beans>
AddressDao defines the contract between the persistence layer and the classes that will orchestrate the reading/writing and deletion of Address beans.
package crud.dao;
import crud.model.Address;
import java.util.List;
/**
* Data Access Object (DAO) interface for persistence of
* {@link crud.model.Address} objects.
*/
public interface AddressDao
{
/** read an address from the data store.
* @param addr this is used as a "find-by-example" bean.
*/
List getAddress( Address addr );
/** create or update an address in the data store.
* @param addr this address will be created if not already there.
*/
void updateAddress( Address addr );
/** delete an Address in the data store.
* @return the number of affected records.
*/
int deleteAddress( Address addr );
}
AddressDaoHibernate is an implementation of AddressDao using Hibernate for object relational mapping.
package crud.dao;
import crud.model.Address;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.SessionFactory;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Implementation of the {@link AddressDao} interface which interacts with
* Spring and Hibernate to map a {@link crud.model.Address} object to the
* AddressBook table in the data store. This class provides a
* basic set of CRUD methods for accessing the address table.
*
* @see crud.model.Address
*/
public final class AddressDaoHibernate extends HibernateDaoSupport
implements AddressDao
{
/** Remove an address.
* @param addr the address to be removed.
* @see crud.model.Address
* @return a count of the affected rows.
*/
public int deleteAddress( Address addr )
{
String hql = "delete from Address where id = :id";
Query query = getSession().createQuery(hql);
query.setLong("id", id );
return query.executeUpdate();
}
/** Save or update an address in the data store.
* @param addr the Address to be saved.
* @see crud.model.Address
* @return a count of the affected rows.
*/
public void updateAddress( Address addr )
{
getHibernateTemplate().saveOrUpdate(addr);
}
/**
* Search for address records with the given name value in
* the Address.name field.
* @return a list of Addresses that match the search criteria.
* @see crud.model.Address
*/
public List getAddress( Address addr )
{
final List list = new ArrayList();
if( addr == null )
return list;
Criteria criteria = getSession().createCriteria(Address.class);
if(addr.getName() != null && addr.getName().length() > 0 )
{
criteria.add(Restrictions.eq("name", addr.getName() ) );
}
return criteria.list();
}
}
Mapping a bean to a relational database using hibernate has traditionally been performed by creating a mapping file in XML. In that approach an Address.hbm.xml file would be created that mapped the database, table and column names to the fields in an Address bean. That is, there is a one-to-one correspondence between a bean to be mapped and a hibernate configuration file. This can become an error-prone burden when the number of mapped beans becomes large.
Annotations simplify the relationship between a bean and hibernate. Annotations are metadata for Java source files that permit the developer to assign these table/column mappings within the source file of the bean to be mapped. They are a special kind of modifier that removes the requirement of writing such boiler-plate code as Classname.hbm.xml for each bean to be persisted. Annotations enable a hibernate supplied library to generate these mappings directly from the Address bean source file.
The listing below is illustrates the application of hibernate annotations to the Address bean.
package crud.model;
import javax.persistence.*;
/**
* The name of a person and their address.
*/
@Entity
public class Address
{
String address;
String name;
String city;
String state;
String zipcode;
/** a unique, database generated, identifier for this record.
*/
private Long id;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
public Long getId() { return this.id; }
public void setId(Long l) { this.id = l; }
public void setName(String s ) { this.name = s; }
public void setAddress(String s ) { this.address = s; }
public void setCity(String s ) { this.city = s; }
public void setState(String s ) { this.state = s; }
public void setZipcode(String s ) { this.zipcode = s; }
@Column (length=32)
public String getName() { return this.name; }
public String getAddress() { return this.address; }
@Column (length=32)
public String getCity() { return this.city; }
@Column (length=2)
public String getState() { return this.state; }
public String getZipcode() { return this.zipcode; }
public String toString()
{
return this.name + "," + this.address + "," + this.city
+ "," + this.state + "," + this.zipcode;
}
}
In the listing of the Address bean above, the @Entity annotation declares the class as an Entity (persistent) bean. @Id declares the primary key field for the table and also declares it as a database generated index value (an Identity field). Column sizes are declared with the @Column attribute. Notice that in the example above that the table name was not specified (there is a @Table annotation for this). By default the table will have the same name as the class.
Note: To enable JDK5 annotations in a java class you must import the java persistence classes: import javax.persistence.*;
Additional functionality is added to the Struts Action class. The new and updated methods are:
find() - a find-by-example search actionremove() - an address record removal actionupdate() - modified to use the DAO object to actually update an Address record.setAddressDao() - A spring-injection accessor to receive the Address data access object.package crud;
import crud.dao.AddressDao;
import crud.model.Address;
import crud.model.StatesList;
import java.util.Collection;
import java.util.List;
import com.opensymphony.xwork2.ActionSupport;
/**
* Address form handler.
*/
public class AddressBookAction extends ActionSupport
{
static final long serialVersionUID = -726287915382955298L;
/** adddress information is stored in this object.
* @see crud.model.Address
*/
private Address address;
/** provider of data access to the Address records table.
*/
private AddressDao dao;
/** The Spring injected Address data access object.
*/
public void setAddressDao(AddressDao dao)
{
this.dao = dao;
}
/** A collection of US States.
* @see crud.model.StatesList
*/
StatesList statesList;
/** Spring injected list of states.
* @param a list of State objects.
* @see crud.model.StatesList
* @see crud.model.State
*/
public void setStatesList( StatesList list )
{
this.statesList = list;
}
/** Accessor for a collection os US States names and abbreviations.
* @see crud.model.States
* @see crud.model.State
* @return a collection of States.
*/
public StatesList getStatesList()
{
return this.statesList;
}
/** Accessor for the address entry object.
* @see crud.model.Address
*/
public Address getAddress()
{
return this.address;
}
/** Accessor for the address entry object.
* @see crud.model.Address
*/
public void setAddress( Address ae )
{
this.address = ae;
}
/** reset (clear) the current Address.
*/
public String reset() throws Exception
{
super.clearErrorsAndMessages();
this.address = new Address();
setMessage( getText(MESSAGE) );
return SUCCESS;
}
/** update (save) the current Address.
*/
public String update() throws Exception
{
dao.updateAddress( this.address );
setMessage( getText(ADDRESS_SAVED) );
return SUCCESS;
}
/** find an address (by example).
*/
public String find() throws Exception
{
List list = dao.getAddress( this.address );
if( list.size() > 0 )
setAddress( (Address) list.get(0) );
setMessage(getText(ADDRESS_FOUND, "0"
, new String[] { String.valueOf(list.size() ) }));
return SUCCESS;
}
/** remove an address.
*/
public String remove() throws Exception
{
int rowCount = dao.deleteAddress( this.address );
setMessage(getText(ADDRESS_DELETED, "0"
, new String[] { String.valueOf(rowCount) }));
return SUCCESS;
}
public String execute() throws Exception
{
setMessage(getText(MESSAGE));
return SUCCESS;
}
/** Default 'ready' message.
*/
public static final String MESSAGE = "addressbook.default.message";
/** Operation 'successful' message.
*/
public static final String ADDRESS_SAVED = "addressbook.updated.message";
/** Delete operation 'successful' message.
*/
public static final String ADDRESS_DELETED = "addressbook.deleted.message";
/** Find operation 'successful' message.
*/
public static final String ADDRESS_FOUND = "addressbook.found.message";
/**
* Field for Message property.
*/
private String message;
/** Accessor for the Message property.
* @return Message property
*/
public String getMessage() { return message; }
/** Accessor for the Message property.
* @param message Text to display on the AddressForm page.
*/
public void setMessage(String message) { this.message = message; }
}
The new message keys must be added to the package.proeprties file.
requiredstring = ${getText(fieldName)} is required.
validator.fieldFormat.zipcode = ${getText(fieldName)} - must be 5 numbers.
validator.fieldFormat.state = ${getText(fieldName)} - must be 2 upper case letters
addressbook.default.message= Address book is ready ...
addressbook.updated.message= Address record updated.
addressbook.deleted.message= {0} record(s) deleted.
addressbook.found.message= {0} record(s) found.
address.form.title=Address Form
address.address = Address:
address.city = City:
address.name = Name:
address.state = State:
address.zipcode = Zipcode:
button.delete=Delete
button.find=Find
button.reset=Reset
button.save=Save
button.update=Update
As you might expect, out-of-container unit testing with DAO objects increases the complexity of the AddressBookActionTest. The in-container test is able to take advantage of the existing servlet container to configure hibernate and inject a DAO object into AddressBookAction. Configuring those services for the out-of-container test will require some additional setup. Fortunately, the Spring framework includes classes to make this much easier than one might expect it to be.
The modifications made to AddressBookActionTest from step 2 are:
AddressBookActionTest extends AbstractDependencyInjectionSpringContextTests.AbstractDependencyInjectionSpringContextTests.getConfiguration to inform the parent class of the location of our Spring configuration file applicationContext.xml.setAddressDao() accessor to receive the DAO class from Spring.build/test/classes directory. This places the Spring configuration file in the classpath of our testspackage crud;
import crud.model.Address;
import crud.dao.AddressDao;
import crud.dao.AddressDaoHibernate;
import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
import com.opensymphony.xwork2.ActionSupport;
import junit.framework.TestCase;
/** Perform tests on crud.AddressBookAction.
* @author R. Kevin Cole
*/
public class AddressBookActionTest extends AbstractDependencyInjectionSpringContextTests
{
private static final Address TEST_ADDRESS = new Address();
private static final Address FIND_ADDRESS = new Address();
static
{
TEST_ADDRESS.setName("R. K. Cole");
TEST_ADDRESS.setAddress("1234 2nd St.");
TEST_ADDRESS.setCity("Nashville");
TEST_ADDRESS.setState("TN");
TEST_ADDRESS.setZipcode("54321");
FIND_ADDRESS.setName( TEST_ADDRESS.getName() );
}
/** specifies the Spring configuration to load for this test fixture.
*/
protected String[] getConfigLocations()
{
return new String[] { "classpath:crud/applicationContext.xml" };
}
AddressDao dao;
/** accessor for the Spring injected data access object.
*/
public void setAddressDao( AddressDao dao )
{
this.dao = dao;
}
/** Prepare an instance of the AddressBookAction class by
* injecting the dao class.
*/
private AddressBookAction getPreparedAddressBookAction()
{
AddressBookAction action = new AddressBookAction();
action.setAddressDao( this.dao );
return action;
}
/** Test that the action is created and the default message is set.
*/
public void testAddressBookAction() throws Exception
{
AddressBookAction addressBook = new AddressBookAction();
String result = addressBook.execute();
assertTrue("Expected SUCCESS", ActionSupport.SUCCESS.equals(result));
assertTrue("Expected default message",
addressBook.getText(AddressBookAction.MESSAGE).equals(addressBook.getMessage()));
}
/** When the setAddress message is called on the AddressBookAction, the getAddress method
* should return that address.
*/
public void testAddress() throws Exception
{
AddressBookAction addressBook = new AddressBookAction();
addressBook.setAddress( TEST_ADDRESS );
assertTrue("Expected a valid name"
, TEST_ADDRESS.getName().equals(addressBook.getAddress().getName()) );
assertTrue("Expected a valid address"
, TEST_ADDRESS.getAddress().equals(addressBook.getAddress().getAddress()) );
assertTrue("Expected a valid city"
, TEST_ADDRESS.getCity().equals(addressBook.getAddress().getCity()) );
assertTrue("Expected a valid state"
, TEST_ADDRESS.getState().equals(addressBook.getAddress().getState()) );
assertTrue("Expected a valid zipcode"
, TEST_ADDRESS.getZipcode().equals(addressBook.getAddress().getZipcode()) );
}
/** When the reset method is called, the address bean is set to null and the default message
* should be set.
*/
public void testReset() throws Exception
{
AddressBookAction addressBook = new AddressBookAction();
addressBook.setAddress( TEST_ADDRESS );
addressBook.reset();
assertTrue("Expected null address", addressBook.getAddress() == null );
assertTrue("Expected default message"
, addressBook.getText(AddressBookAction.MESSAGE).equals(addressBook.getMessage()));
}
/** Test the action-update method by writing the TEST_ADDRESS to the database.
*/
public void testUpdate() throws Exception
{
AddressBookAction addressBook = getPreparedAddressBookAction();
addressBook.setAddress( TEST_ADDRESS );
String result = addressBook.update();
assertTrue("Expected SUCCESS", result == ActionSupport.SUCCESS );
assertTrue("Expected unchanged address", addressBook.getAddress() == TEST_ADDRESS );
assertTrue("Expected SAVED message",
addressBook.getText(AddressBookAction.ADDRESS_SAVED).equals(addressBook.getMessage()));
addressBook.reset();
assertTrue("Expected a null address", addressBook.getAddress() == null );
}
/** Test the find method by searching for the FIND_ADDRESS in the database.
*/
public void testFind() throws Exception
{
AddressBookAction addressBook = getPreparedAddressBookAction();
addressBook.setAddress( FIND_ADDRESS );
String result = addressBook.find();
assertTrue("Expected SUCCESS", result == ActionSupport.SUCCESS );
assertTrue("Expected a valid name"
, TEST_ADDRESS.getName().equals(addressBook.getAddress().getName()) );
assertTrue("Expected a valid address"
, TEST_ADDRESS.getAddress().equals(addressBook.getAddress().getAddress()) );
assertTrue("Expected a valid city"
, TEST_ADDRESS.getCity().equals(addressBook.getAddress().getCity()) );
assertTrue("Expected a valid state"
, TEST_ADDRESS.getState().equals(addressBook.getAddress().getState()) );
assertTrue("Expected a valid zipcode"
, TEST_ADDRESS.getZipcode().equals(addressBook.getAddress().getZipcode()) );
}
/** Find the test-address record and remove it.
*/
public void testRemove() throws Exception
{
AddressBookAction addressBook = getPreparedAddressBookAction();
addressBook.setAddress( FIND_ADDRESS );
String result = addressBook.find();
assertTrue("Expected SUCCESS", result == ActionSupport.SUCCESS );
assertTrue("Expected valid address ID", addressBook.getAddress().getId() > 0 );
result = addressBook.remove();
assertTrue("Expected null address", addressBook.getAddress() == null );
}
}
Modifications to the in-container test are not as extensive. Support for testing the "find" and "remove" action methods are added.
package crud;
import crud.model.Address;
import net.sourceforge.jwebunit.junit.WebTestCase;
/** Perform in-container tests on crud.AddressBookAction.
* @author R. Kevin Cole
*/
public class AddressBookWebTest extends WebTestCase
{
static Address TEST_ADDRESS = new Address();
static
{
TEST_ADDRESS.setName("R. K. Cole");
TEST_ADDRESS.setAddress("1234 2nd St.");
TEST_ADDRESS.setCity("Nashville");
TEST_ADDRESS.setState("TN");
TEST_ADDRESS.setZipcode("54321");
}
public void setUp()
{
getTestContext().setBaseUrl("http://localhost:8080/Struts2CrudStep3");
getTestContext().setResourceBundleName("package");
}
/** Test that the address form page appears and that the form
* contains the correct fields for address entry.
*/
public void testAddressFormPage() throws Exception
{
beginAt("/");
assertTitleEquals("Address Form");
assertFormPresent("AddressBook");
assertFormElementPresent("address.name");
assertFormElementPresent("address.address");
assertFormElementPresent("address.city");
assertFormElementPresent("address.state");
assertFormElementPresent("address.zipcode");
// check that the state field is initially blank (the blank option is on).
assertSelectedOptionsEqual("address.state", new String[] {""});
}
/** Test the "update" button.
*/
public void testUpdate() throws Exception
{
beginAt("/");
assertTitleEquals("Address Form");
setWorkingForm("AddressBook");
setTextField("address.name", TEST_ADDRESS.getName() );
setTextField("address.address", TEST_ADDRESS.getAddress() );
setTextField("address.city", TEST_ADDRESS.getCity() );
selectOptionByValue("address.state", TEST_ADDRESS.getState() );
setTextField("address.zipcode", TEST_ADDRESS.getZipcode() );
submit("method:update");
assertKeyPresent("addressbook.updated.message");
assertTextFieldEquals("address.name", TEST_ADDRESS.getName() );
assertTextFieldEquals("address.address", TEST_ADDRESS.getAddress() );
assertTextFieldEquals("address.city", TEST_ADDRESS.getCity() );
assertSelectedOptionValueEquals("address.state", TEST_ADDRESS.getState() );
assertTextFieldEquals("address.zipcode", TEST_ADDRESS.getZipcode() );
}
/** Test the "find" button.
*/
public void testFind() throws Exception
{
beginAt("/");
assertTitleEquals("Address Form");
setWorkingForm("AddressBook");
setTextField("address.name", TEST_ADDRESS.getName() );
submit("method:find");
assertTextFieldEquals("address.name", TEST_ADDRESS.getName() );
assertTextFieldEquals("address.address", TEST_ADDRESS.getAddress() );
assertTextFieldEquals("address.city", TEST_ADDRESS.getCity() );
assertSelectedOptionValueEquals("address.state", TEST_ADDRESS.getState() );
assertTextFieldEquals("address.zipcode", TEST_ADDRESS.getZipcode() );
}
/** Test the "reset" button.
*/
public void testReset() throws Exception
{
beginAt("/");
assertTitleEquals("Address Form");
setWorkingForm("AddressBook");
setTextField("address.name", TEST_ADDRESS.getName() );
setTextField("address.address", TEST_ADDRESS.getAddress() );
setTextField("address.city", TEST_ADDRESS.getCity() );
selectOptionByValue("address.state", TEST_ADDRESS.getState() );
setTextField("address.zipcode", TEST_ADDRESS.getZipcode() );
submit("method:reset");
assertKeyPresent("addressbook.default.message");
assertTextFieldEquals("address.name", "");
assertTextFieldEquals("address.address", "");
assertTextFieldEquals("address.city", "");
assertSelectedOptionsEqual("address.state", new String[] {""});
assertTextFieldEquals("address.zipcode", "");
}
/** Test the "remove" button.
*/
public void testRemove() throws Exception
{
beginAt("/");
assertTitleEquals("Address Form");
setWorkingForm("AddressBook");
setTextField("address.name", TEST_ADDRESS.getName() );
submit("method:find");
assertTextFieldEquals("address.name", TEST_ADDRESS.getName() );
assertTextFieldEquals("address.address", TEST_ADDRESS.getAddress() );
assertTextFieldEquals("address.city", TEST_ADDRESS.getCity() );
assertSelectedOptionValueEquals("address.state", TEST_ADDRESS.getState() );
assertTextFieldEquals("address.zipcode", TEST_ADDRESS.getZipcode() );
submit("method:remove");
assertTextFieldEquals("address.name", "");
assertTextFieldEquals("address.address", "");
assertTextFieldEquals("address.city", "");
assertSelectedOptionsEqual("address.state", new String[] {""});
assertTextFieldEquals("address.zipcode", "");
}
}
Two new submit-buttons, Find and Remove are added to the address-form. These buttons are supported by the new find() and remove() methods in AddressBookAction class.
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<link href="skin.css" rel="stylesheet" type="text/css">
<title><s:text name="address.form.title"/></title>
</head>
<body>
<div class="form-container">
<h1><s:text name="address.form.title"/></h1>
<div class="error"><s:fielderror /></div>
<s:form action="AddressBook" theme="simple">
<s:hidden key="address.id"/>
<table><tr>
<th>
<s:label key="getText('address.name')"/>
</th><td colspan="2">
<s:textfield size="24" key="address.name" name="address.name"/>
</td>
</tr><tr>
<th>
<s:label key="getText('address.address')"/>
</th><td colspan="2">
<s:textfield size="32" key="address.address" name="address.address"/>
</td>
</tr><tr>
<th>
<s:label key="getText('address.city')"/>
</th><td colspan="2">
<s:textfield size="18" key="address.city" name="address.city"/>
</td>
</tr><tr>
</tr><tr>
<th>
<s:label key="getText('address.state')"/>
</th><td colspan="2">
<s:select
label="State"
list="statesList"
name="address.state"
listKey="id"
listValue="name"
emptyOption="true"
/>
</td>
</tr><tr>
<th>
<s:label key="getText('address.zipcode')"/>
</th><td colspan="2">
<s:textfield size="5" key="address.zipcode" name="address.zipcode"/>
</td>
</tr><tr>
<td class="buttonbar" colspan="3">
<s:submit key="button.reset" method="reset" cssClass="button"/>
<s:submit key="button.save" method="update" cssClass="button"/>
<s:submit key="button.find" method="find" cssClass="button"/>
<s:submit key="button.delete" method="remove" cssClass="button"/>
</td>
</tr></table>
</s:form>
<div class="statusbar">
<s:property value="message"/>
</div>
</div>
</body>
</html>
step3>ant test-all
Buildfile: build.xml
init:
compile:
[javac] Compiling 1 source file to \step3\build\test\classes
compile-tests:
[javac] Compiling 1 source file to \step3\build\test\classes
test:
[junit] Testsuite: crud.AddressBookActionTest
[junit] Tests run: 6, Failures: 0, Errors: 0, Time elapsed: 2.344 sec
[junit]
test-all:
init:
compile:
[javac] Compiling 1 source file to \step3\build\test\classes
compile-tests:
[javac] Compiling 1 source file to \step3\build\test\classes
test:
[junit] Testsuite: crud.AddressBookWebTest
[junit] Tests run: 5, Failures: 0, Errors: 0, Time elapsed: 3.89 sec
[junit]
BUILD SUCCESSFUL
Total time: 11 seconds
step3>
This completes step 3 in the Struts CRUD Tutorial. In this step, the address records were persisted using the Hibernate ORM tool.