Step 1: Getting Started
R. Kevin ColeMarch 12, 2007

This tutorial is a multipart introduction to the development of a Create, Read, Update and Delete (CRUD) application using the Struts-2 framework. The application is a simple address-book.

Developers new to Struts 2 are the target of this tutorial. It is assumed that the reader is familiar with a web framework such as Struts 1, J2EE technologies such as JDBC, and is comfortable working with the Apache ant build tool. It was written by the author as a first exercise in developing a Struts-2 web application.

In this first part of the tutorial, the following subjects are covered.

  1. Setup the development and test environments.. A project directory structure is created and equipped with an ant build file to build and maintain the project.
  2. The unit test classes. Two unit test classes are created to test the Struts action and the address bean inside and outside a servlet container.
  3. The Server-side Action and Data Bean The server-side struts action class, a validator and a data transfer bean area created.
  4. The Client-side Address-Form An address data entry form with some minimal css decorating is created along with a corresponding action and address bean to move address-data between the address-view and the server.
  5. Deploy and Test the Application Tests are run to verify that data is collected from the address form on the client page and that an address bean is created and successfully populated on the server.

Setup the development environment.

This tutorial requires a Struts-2 binary distribution and must be run in J2SE version 5. or later. The servlet container used in the examples is Tomcat 5.

The project directory is setup and our struts configuration files are created.

  1. Create the Project Directory
  2. Create struts.xml and crud.xml
  3. Create an Ant Build file
  4. Setup the Test Environment

Create the Project Directory

The source code directory is split into two parts. The main code that runs the website and the test code that implements the unit tests. Create a directory tree like the one shown on the left. The ant build file expects this type of structure but ant is quite flexible should you want to do things in another manner; just be certain to change build.xml to reflect your directory structure.

The lib directory holds the jar files to be deployed with the web application in a war file. For step 1 of this tutorial, the lib directory is a copy of the jar files found in the struts-blank.war demonstration application in its WEB-INF/lib directory.

A second directory lib.test will hold jar files that are required for unit testing the application. The jar files in lib.test are not deployed in the war file to the application server. The jar files in lib.test are not included in the struts-blank.war and will have to be downloaded separately.


Create struts.xml and crud.xml

The struts configuration files for the CRUD application are created in the src/main/resources directory. The basic struts.xml configuration does little more than include this tutorial's configuration file crud.xml

Create a basic struts.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
 
<include file="struts-default.xml"/>
 
<constant name="struts.enable.DynamicMethodInvocation" value="false"/>
<constant name="struts.devMode" value="true"/>
 
<include file="crud.xml"/>
 
</struts>

and our project specific crud.xml.

<?xml version="1.0" encoding="UTF-8" ?> 
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
 
<struts>
 
<package name="crud" namespace="/crud" extends="struts-default">
 
<interceptors>
<interceptor-stack name="defaultStack">
<interceptor-ref name="exception" />
<interceptor-ref name="alias" />
<interceptor-ref name="i18n" />
<interceptor-ref name="chain" />
<interceptor-ref name="static-params" />
<interceptor-ref name="params" />
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation">
<param name="excludeMethods">cancel,execute,reset</param>
</interceptor-ref>
<interceptor-ref name="workflow">
<param name="excludeMethods">input,back,cancel</param>
</interceptor-ref>
</interceptor-stack>
</interceptors>
 
<default-interceptor-ref name="defaultStack"/>
 
<action name="AddressBook" class="crud.AddressBookAction">
<result name="input">/crud/AddressForm.jsp</result>
<result>/crud/AddressForm.jsp</result>
</action>
 
<!-- Add actions here -->
</package>
</struts>

Add an Ant Build File

The ant build file included with the example supports the following targets for building the application and maintaining the source tree.

antcompile all source and test files
ant   testcompile and run all out-of-container unit tests
ant   test-webcompile and run all in-container unit tests
ant   test-allcompile and run all unit tests
ant   warcompile all source files and build the war file
ant   cleanremove the build and dist directory trees

Setup the Test Environment

The required libraries for the JUnit and JWebUnit unit testing frameworks are installed in lib.test. (It's a good idea not to mix libraries that you will deploy in your war file with development-only libraries). The following jar files will need to be installed into lib.test.

The Unit Test Classes

We'll start with two unit test cases that will verify the proper functioning of the Struts action and the Address bean.

Container-Independent Test - AddressBookActionTest

The first test class, AddressBookActionTest, tests the Struts action outside the servlet container. The method testAddressBookAction verifies that the AddressBookAction returns SUCCESS when it is executed and checks that we can get and populate an Address bean.

package crud; 

import crud.model.Address;

import com.opensymphony.xwork2.ActionSupport;
import junit.framework.TestCase;

/** Perform tests on crud.AddressBookAction.
*/
public class AddressBookActionTest extends TestCase
{
private static final 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");
}

/** 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 a success result!",
ActionSupport.SUCCESS.equals(result));
assertTrue("Expected the 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()));
}

/** When the update method is called, the address bean is unchanged and the default message
* should be set.
*/
public void testUpdate() throws Exception
{
AddressBookAction addressBook = new AddressBookAction();
addressBook.setAddress( TEST_ADDRESS );
addressBook.update();

assertTrue("Expected an unchanged address", addressBook.getAddress() == TEST_ADDRESS);
assertTrue("Expected the SAVED message!",
addressBook.getText(AddressBookAction.ADDRESS_SAVED).equals(addressBook.getMessage()));
}
}

In-Container Test - AddressBookWebTest

The second test, AddressBookWebTest, checks that the address-form is present on the page and that it has the correct fields and default values. This test is run inside the servlet container. It is a live test of the running website.

package crud; 

import crud.model.Address;

import net.sourceforge.jwebunit.junit.WebTestCase;

/** Perform in-container tests on crud.AddressBookAction.
*/
public class AddressBookWebTest extends WebTestCase
{
private static final 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/Struts2CrudStep1");
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");
}

/** 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() );
setTextField("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() );
assertTextFieldEquals("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() );
setTextField("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", "");
assertTextFieldEquals("address.state", "");
assertTextFieldEquals("address.zipcode", "");
}
}

Neither of the above tests will run at this point. They won't even compile until we create the action and the bean. The tests provide us with an initial contract that will govern the behavior of the classes that produced in the next section.

The Server-side Action and Data Bean.

There are two java classes and an XML validator used to process our address entry form. A struts action called AddressBookAction will handle the setup and processing of the address-form and an address information holder bean called Address will handle the transfer of the address-form data. The validation of the data contained within the Address bean is defined in AddressBookAction-validator.xml

The Address Bean

The address object is a simple bean with accessors for the address and name of a person. The accessor method names have been established by the testing criteria above.

package crud.model;
 
/** The name of a person and their address.
*/
public class Address
{
String address;
String name;
String city;
String state;
String zipcode;

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; }

public String getName() { return this.name; }
public String getAddress() { return this.address; }
public String getCity() { return this.city; }
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;
}
}

The AddressBookAction

The AddressBookAction includes two methods that are called when buttons are pressed in the address-form. These are reset() and udpate(). Each button in the address-form will be assigned a method in this class. In this step, the update() method sets a message for display by the address-form.

package crud;
 
import crud.model.Address;
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
*/
Address address;

public String execute() throws Exception
{
setMessage(getText(MESSAGE));
return SUCCESS;
}
 
/** Get the address entry object.
* @see crud.model.Address
*/
public Address getAddress()
{
return this.address;
}
 
/** Set 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.
* No-Operation in this example.
*/
public String update() throws Exception
{
setMessage( getText(ADDRESS_SAVED) );
return SUCCESS;
}

 
/** Default 'ready' message.
*/
public static final String MESSAGE = "addressbook.default.message";

 
/** Operation 'successful' message.
*/
public static final String ADDRESS_SAVED = "address.updated.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 s) { this.message = s; }
}

Configuring a Validator

Since all fields in our address form are required, a field-validator of requiredstring is attached to each. The zipcode field gets an additional validation to ensure that it is exactly 5 digits. A server-side field validator is attached to the state field to ensure that it is exactly two upper-case characters in the range of A-Z.

In this example, the name=address.name attribute in the field validator informs the Struts-2 framework that the string to be validated is acquired by making the following call on an instance of AddressBookAction:
addressBookAction.getAddress().getName()

<!DOCTYPE validators PUBLIC 
"-//OpenSymphony Group//XWork Validator 1.0.2//EN"
"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
 
<validators>
<field name="address.name">
<field-validator type="requiredstring">
<message key="requiredstring"/>
</field-validator>
</field>
 
<field name="address.address">
<field-validator type="requiredstring">
<message key="requiredstring"/>
</field-validator>
</field>
 
<field name="address.city">
<field-validator type="requiredstring">
<message key="requiredstring"/>
</field-validator>
</field>
 
<field name="address.state">
<field-validator type="requiredstring">
<message key="requiredstring"/>
</field-validator>

<!-- 2 digit state code -->
<field-validator type="regex">
<param name="expression">
[A-Z]{2}
</param>
<message key="validator.fieldFormat.state" />
</field-validator>
</field>
 
<field name="address.zipcode">
<field-validator type="requiredstring">
<message key="requiredstring"/>
</field-validator>

<!-- 5 digit zipcode -->
<field-validator type="regex">
<param name="expression">
[0-9]{5}
</param>
<message key="validator.fieldFormat.zipcode" />
</field-validator>
</field>
 
</validators>

The Address entry form

The address entry form will have five fields and a status-bar. To meet our test requirements, the fields will be called:

  • address.name
  • address.address
  • address.city
  • address.state
  • address.zipcode

The status-bar at the base of the form is populated using the Struts UI property tag.This writes the value returned by a call to AddressBookAction.getMessage() into the status line.

Note about themes: Struts-2 provides a mechanism for decorating forms by the declaring a theme. For instance, the default theme will wrap labels and fields on a form within a table making it remarkably simple to layout a form. This can cause problems with simple things like adding a row of buttons at the base of a form (the default theme wants to lay them out in a column). For that reason, the address-form in this example declares the theme as simple; meaning - don't auto-format the form. The address-form handles its own layout.

Here is the address entry form (with an applied style found in skin.css).

The address-form is produced by this view:

<%@ 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">
<table><tr>
<th>
<s:label key="getText('address.name')"/>
</th><td>
<s:textfield size="24" key="address.name" name="address.name"/>
</td>
</tr><tr>
<th>
<s:label key="getText('address.address')"/>
</th><td">
<s:textfield size="32" key="address.address" name="address.address"/>
</td>
</tr><tr>
<th>
<s:label key="getText('address.city')"/>
</th><td>
<s:textfield size="18" key="address.city" name="address.city"/>
</td>
</tr><tr>
<th>
<s:label key="getText('address.state')"/>
</th><td>
<s:textfield size="2" key="address.state" name="address.state"/>
</td>
</tr><tr>
<th>
<s:label key="getText('address.zipcode')"/>
</th><td>
<s:textfield size="5" key="address.zipcode" name="address.zipcode"/>
</td>
</tr><tr>
<td class="buttonbar" colspan="2">
<s:submit cssClass="button" key="button.reset" method="reset"/>
<s:submit cssClass="button" key="button.save" method="update"/>
</td>
</tr></table>
</s:form>
<div class="statusbar">
<s:property value="message"/>
</div>
</div>
</body>
</html>

Note: In the struts tag textfield, the attribute name maps to a getter/setter pair in the Address bean while the attribute key maps to a line in the package.properties file.

Deploy the Application and Run All Tests

Build the war file by running ant war. Copy the Struts2CrudStep1.war to your web container's webapp directory and point your browser to Struts 2 Crud - Step1.

Run all tests:

step1> ant test-all 
Buildfile: build.xml

init:

compile:
[javac] Compiling 1 source file to X:\demo\Struts2Crud\step1\build\classes
[javac] Compiling 1 source file to X:\demo\Struts2Crud\step1\build\test\classes

compile-tests:
[javac] Compiling 1 source file to X:\demo\Struts2Crud\step1\build\test\classes

test:
[junit] Testsuite: crud.AddressBookActionTest
[junit] Tests run: 4, Failures: 0, Errors: 0, Time elapsed: 0.687 sec
[junit]

test-all:

init:

compile:
[javac] Compiling 1 source file to X:\demo\Struts2Crud\step1\build\classes
[javac] Compiling 1 source file to X:\demo\Struts2Crud\step1\build\test\classes

compile-tests:
[javac] Compiling 1 source file to X:\demo\Struts2Crud\step1\build\test\classes

test:
[junit] Testsuite: crud.AddressBookWebTest
[junit] Tests run: 3, Failures: 0, Errors: 0, Time elapsed: 3.391 sec
[junit]

BUILD SUCCESSFUL
Total time: 9 seconds

step1>

This completes step 1 in the Struts CRUD Tutorial. In this step a simple address book user interface was constructed that accepts address data entered in a browser and sends the data to a Struts-2 action where the data receives some cursory validation.

In step 2, the state textfield on the address-form will be replaced with a drop-down select/options box. This will give us exact control over the contents of the state field. A collection representing the 50 U.S. States will be injected, using Spring's Inversion of Control mechanism, into the AddressBookAction class.