Struts 2 is the new version of Struts, under development at the Apache Group. It is based on WebWork 2, and has many modern web framework features, like user interface tags, type conversion, and validation. Struts 2 is highly flexible and extensible.
In this cookbook you will find HOWTO guides, and recipes for common scenarios in Struts 2.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jasperreports-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>compile-reports</goal>
</goals>
</execution>
</executions>
</plugin>
The plugin expects your jrxml files to be in src/main/jasperreports. The compiled jasper files are placed in target/classses. You can have subdirectories and these will be preserved in the target/classes directory.
public String intercept (ActionInvocation invocation) throws Exception {
// Get the action context from the invocation so we can access the
// HttpServletRequest and HttpSession objects.
final ActionContext context = invocation.getInvocationContext ();
HttpServletRequest request = (HttpServletRequest) context.get(HTTP_REQUEST);
HttpSession session = request.getSession (true);
// Is there a "user" object stored in the user's HttpSession?
Object user = session.getAttribute (USER_HANDLE);
if (user == null) {
// The user has not logged in yet.
// Is the user attempting to log in right now?
String loginAttempt = request.getParameter (LOGIN_ATTEMPT);
if (! StringUtils.isBlank (loginAttempt) ) { // The user is attempting to log in.
// Process the user's login attempt.
if (processLoginAttempt (request, session) ) {
// The login succeeded send them the login-success page.
return "login-success";
} else {
// The login failed. Set an error if we can on the action.
Object action = invocation.getAction ();
if (action instanceof ValidationAware) {
((ValidationAware) action).addActionError ("Username or password incorrect.");
}
}
}
// Either the login attempt failed or the user hasn't tried to login yet,
// and we need to send the login form.
return "login";
} else {
return invocation.invoke ();
}
}
The actual validation of the username and password are left to the processLoginAttempt() method. The processLoginAttempt() method is responsible for using whatever means necessary to validate the username and password and saving the user's user object in the session. This is done this way, because there might be many ways to do the validation, and you might collect additional information on the user to store in their sesion, such as roles, or other credentials. Breaking this out allows you to override this method in your own interceptor and process this however you'd like.
The default implementation uses a SecurityManager to lookup the user's information.
/**
* Attempt to process the user's login attempt delegating the work to the
* SecurityManager.
*/
public boolean processLoginAttempt (HttpServletRequest request, HttpSession session) {
// Get the username and password submitted by the user from the HttpRequest.
String username = request.getParameter (USERNAME);
String password = request.getParameter (PASSWORD);
// Use the security manager to validate the user's username and password.
Object user = securityManager.login (username, password);
if (user != null) {
// The user has successfully logged in. Store their user object in
// their HttpSession. Then return true.
session.setAttribute (USER_HANDLE, user);
return true;
} else {
// The user did not successfully log in. Return false.
return false;
}
}
<struts>
<package name="my-default" extends="struts-default">
<interceptors>
...
<interceptor name="login" class="loginInterceptor" />
...
</interceptors>
</package>
</struts>
Notice that the fully qualified classname is not declared for the interceptor. We configure that in our Spring config, which allows us to wire our SecurityManager into it.
<struts>
<package name="my-default" extends="struts-default">
<interceptors>
...
<interceptor-stack name="defaultLoginStack">
<interceptor-ref name="servlet-config" />
<interceptor-ref name="params" />
<interceptor-ref name="login" /> <!-- Our LoginInterceptor -->
<interceptor-ref name="prepare" />
<interceptor-ref name="chain" />
<interceptor-ref name="model-driven" />
<interceptor-ref name="fileUpload" />
<interceptor-ref name="static-params" />
<interceptor-ref name="params" />
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation" />
<interceptor-ref name="workflow" />
</interceptor-stack>
...
</package>
</struts>
<struts>
<package name="my-default" extends="struts-default">
...
<global-results>
<result name="login">/WEB-INF/pages/Login.jsp</result>
<result name="login-success">/WEB-INF/pages/Index.jsp</result>
</global-results>
...
</package>
</struts>
<struts>
<package name="my-default" extends="struts-default">
<interceptors>
<interceptor name="login" class="loginInterceptor" />
<interceptor-stack name="defaultLoginStack">
<interceptor-ref name="servlet-config" />
<interceptor-ref name="params" />
<interceptor-ref name="login" />
<interceptor-ref name="prepare" />
<interceptor-ref name="chain" />
<interceptor-ref name="model-driven" />
<interceptor-ref name="fileUpload" />
<interceptor-ref name="static-params" />
<interceptor-ref name="params" />
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation" />
<interceptor-ref name="workflow" />
</interceptor-stack>
<interceptor-stack name="defaultInsecureStack">
<interceptor-ref name="servlet-config" />
<interceptor-ref name="params" />
<interceptor-ref name="prepare" />
<interceptor-ref name="chain" />
<interceptor-ref name="model-driven" />
<interceptor-ref name="fileUpload" />
<interceptor-ref name="static-params" />
<interceptor-ref name="params" />
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation" />
<interceptor-ref name="workflow" />
</interceptor-stack>
</interceptors>
<!-- Make the defaultLoginStack the default one used
for all actions unless otherwise configured. -->
<default-interceptor-ref name="defaultLoginStack" />
<default-action-ref name="index" />
<global-results>
<result name="login">/WEB-INF/pages/Login.jsp</result>
<result name="login-success">/WEB-INF/pages/Index.jsp</result>
</global-results>
<action name="index">
<result name="input">/WEB-INF/pages/Index.jsp</result>
<result name="success">/WEB-INF/pages/Index.jsp</result>
</action>
</package>
</struts>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Login</title>
</head>
<body>
<s:actionerror />
<s:form namespace="/" action="index.action" method="post">
<s:hidden name="QUADRAN_LOGIN_ATTEMPT" value="%{'1'}" />
<s:textfield name="QUADRAN_USERNAME" label="Username" />
<s:password name="QUADRAN_PASSWORD" label="Password" />
<s:submit value="Login" align="center">
<s:param name="colspan" value="%{2}" />
<s:param name="align" value="%{'center'}" />
</s:submit>
</s:form>
</body>
</html>
Notice the hidden QUADRAN_LOGIN_ATTEMP property in the form. That is what triggers the processing of the login by the LoginInterceptor.
package net.vitarara.quadran.core.business.api.security;
public interface SecurityManager {
public Object login (String username, String password);
}
And the implementation in Groovy:
package net.vitarara.quadran.core.business.impl.security;
import net.vitarara.quadran.core.business.api.security.SecurityManager;
class SecurityManagerImpl implements SecurityManager {
def partyDao;
Object login (String username, String password) {
return partyDao.findByUsernameAndPassword (username, password);
}
}
I delegate the lookup to my PartyDao which is a class for looking up all types of parties, users, customers, vendors, etc. You will need to figure out how you want to look users up in your system.
<bean id="loginInterceptor" scope="singleton" class="net.vitarara.quadran.core.web.util.LoginInterceptor">
<property name="securityManager"><ref local="securityManager" /></property>
</bean>
If you are using the Struts 2 Spring plugin whe Struts attempts to load the interceptor at startup it will use Spring to instantiate it.
class Person {
String firstName, lastName, address1, address2, city, state, zip, country;
}
That creates a bean with a default no arg constructor. All of the String members that I have declared, firstName, etc., have a private scope, and Groovy create a setXxx() and a getXxx() for each member. For more information see Groovy Beans.
What does this get us in Struts 2? Simple actions. Here's an example of an action that accepts an id as a request parameter, uses a service bean to look up an order with that id, and stores it for retrieval in the presentation layer (JPS's, FreeMarker, etc.).
package example;
import com.opensymphony.xwork2.ActionSupport;
public class MyAction extends ActionSupport {
// Service Bean
def myServiceBean;
// Model Beans
String id; // Id of an order in the database to go look up.
def order; // The order we're going to display.
String execute () throws Exception {
order = myServiceBean.findOrder (id);
return SUCCESS;
}
}
Simple isn't it. No setters and getters cluttering the code. Groovy creates all of those for you. Also the service bean can be injected using Spring. (Just be sure it has a findOrder method, or you will get an Exception. You need to test for that with your integration tests.)
One trick is to make sure that you declare the type of the parameters you want to take in from the http request. That will allow Strut 2 to do its type conversion for you. By declaring the id property of type String it causes Struts 2 to populate that property from the http request with a String. If you wanted it to be an int you could also declare it as an int.
Simple isn't it. It also looks a lot like Java too. So, why would you want to do this? For me the motivation was search forms that did not map to a domain model object. Frequently these had a lot of fields on them. Searching for a purchase order you might want to be able to search by customer, person who entered it, date entered from, date entered to, date of shipping from, date of shipping to, product, and the list could go on. In Java you would need a setter and getter for each of these form properties. (I've tried using Map backed forms in Struts 2, but you loose the type conversion.) In Groovy this Action could be very small.
package net.vitarara.quadran.core.web.order.purchasing;
import com.opensymphony.xwork2.Preparable;
import net.vitarara.quadran.core.business.api.purchasing.PurchasingManager;
import net.vitarara.quadran.core.web.QActionSupport;
import net.vitarara.utils.VRContext;
import org.apache.commons.lang.StringUtils;
class ListPurchaseOrdersAction extends QActionSupport implements Preparable {
// Service Beans
PurchasingManager purchasingManager;
// Model Bean(s)
List carriers, issuers, destinationAreas, products, purchaseOrders, vendors;
// Form Properties
Date dateEnteredFrom, dateEnteredTo, dateOfReceiptFrom, dateOfReceiptTo;
String status, issuedById, orderNumber, vendorId;
String destinationAreaId, carrierId, productId, fob;
Boolean dutyPrepaid;
void prepare () throws Exception {
// Get the data for the drop downs.
carriers = purchasingManager.getCarriers ();
destinationAreas = purchasingManager.getDestinationAreas ();
products = purchasingManager.getProducts ();
vendors = purchasingManager.getVendors ();
issuers = purchasingManager.getIssuers ();
}
/**
* Marshall that data from our form input to the service layer to
* find purchase orders.
*/
String execute () throws Exception {
if (isFormFilledIn () ) {
VRContext context = new VRContext ();
context.dateEnteredFrom = dateEnteredFrom;
context.dateEnteredTo = dateEnteredTo;
context.dateOfReceiptFrom = dateOfReceiptFrom;
context.dateOfReceiptTo = dateOfReceiptTo;
context.status = status;
context.issuedById = issuedById;
context.orderNumber = orderNumber;
context.vendorId = vendorId;
context.destinationAreaId = destinationAreaId;
context.carrierId = carrierId;
context.productId = productId;
context.fob = fob;
context.dutyPrepaid = dutyPrepaid;
context = purchasingManager.listPurchaseOrders (context);
this.purchaseOrders = context.purchaseOrders;
}
return SUCCESS;
}
/** Determine if the user has filled out web the form. */
boolean isFormFilledIn () {
if (status || issuedById || orderNumber || vendorId ||
destinationAreaId || carrierId ||
productId || fob || dateEnteredFrom || dateEnteredTo ||
dateOfReceiptFrom || dateOfReceiptTo ) {
return true;
} else {
return false;
}
}
}
This example is definitely more complicated. First and foremost for me, the vi loving guy, I didn't have to write 20 sets of setter and getter methods. That's about 80 lines of saved typing. All of the code in the class actually does something or declares the existence of a property of the class. The boilerplate code is gone.
This action still uses the type conversion that Struts 2 provides. When the user fills in the dateEnteredFrom field on the HTML form, Struts 2 will convert that to a Date object for me, because dateEnteredFrom is of type java.util.Date. Also notice I didn't need to import java.util.blah blah blah... How many time have I wished javac just imported it. No need to worry, Groovy does.
Walking through the example. The class starts with property declaration. The purchasingManger is injected using Spring in my application. The rest of the properties are either used for display purposes in the form, or are used to collect user input.
The prepare() method is called as normal. Notice my Groovy class implements the Preparable interface, and extends QActionSupport, both of which are Java classes.
In my execute I marshal the users input from my Action into a VRContext, which implements Map. Notice I use very OGNL like notation to put the values into the map, map.somekey = value.
In the isFormFilledIn() method I check to see if the user has filled in the form. Notice that if structure, if (somestring), no need to check for null, no need for something like StringUtils.isBlank, etc.
Short sweet, and everything means something.
If you're using the Struts 2 maven archetype, or Maven in general you can use it to compile your Groovy files to .class files. Groovy Maven Wiki Entry
I'm just getting started with Groovy. If you're a Groovy master, and have pointers, please post them.
This JSP markup to create this form is quite simple, and is a testament to the power of the Struts 2 UI Tags:
<s:form action="some.action" method="post" theme="%{currentTheme}">
<s:textfield label="First Name" name="firstName"/>
<s:textfield label="Last Name" name="lastName"/>
<s:textfield label="Address 1" name="address1"/>
<s:textfield label="City" name="city"/>
<s:textfield label="State" name="state"/>
<s:textfield label="Zip" name="postalCode"/>
<s:submit value="Create Party" align="center">
<s:param name="colspan" value="%{tableColSpan}" />
<s:param name="align" value="%{'center'}" />
</s:submit>
</s:form>
As you can see the template contains no HTML markup at all. All of the layout is done by the tags, and the templates that back them. Using these tags you can save an enormous amount of time writing table, tr and td tags.
The issue though is that many web applications contain forms that are far more complicated than are accommodated using a two column layout. Sometimes we need to do column spans, row spans, and use more then two columns.
Sometimes we need a form that looks like this:
This is an eight (8) column layout from a project I'm currently working on called Quadran. An excerpt from the JSP source code:
<s:textfield label="Requested Delivery Date" name="order.dutyPrepaid">
<s:param name="labelcolspan" value="%{2}" />
<s:param name="inputcolspan" value="%{2}" />
</s:textfield>
<s:textfield label="Third Party Bill To" name="order.dutyPrepaid">
<s:param name="labelcolspan" value="%{2}" />
<s:param name="inputcolspan" value="%{2}" />
</s:textfield>
<s:textfield label="Trucker" name="order.dutyPrepaid" size="70">
<s:param name="labelcolspan" value="%{2}" />
<s:param name="inputcolspan" value="%{6}" />
</s:textfield>
<s:textfield label="Loading Instructions" name="order.dutyPrepaid" size="70">
<s:param name="labelcolspan" value="%{2}" />
<s:param name="inputcolspan" value="%{6}" />
</s:textfield>
<s:textfield label="Shipping Instructions" name="order.dutyPrepaid" size="70">
<s:param name="labelcolspan" value="%{2}" />
<s:param name="inputcolspan" value="%{6}" />
</s:textfield>
<input type="text"<#rt/>
name="${parameters.name?default("")?html}"<#rt/>
<#if parameters.get("size")?exists>
size="${parameters.get("size")?html}"<#rt/>
</#if>
<#if parameters.maxlength?exists>
maxlength="${parameters.maxlength?html}"<#rt/>
</#if>
<#if parameters.nameValue?exists>
value="<@s.property value="parameters.nameValue"/>"<#rt/>
</#if>
<#if parameters.disabled?default(false)>
disabled="disabled"<#rt/>
</#if>
<#if parameters.readonly?default(false)>
readonly="readonly"<#rt/>
</#if>
<#if parameters.tabindex?exists>
tabindex="${parameters.tabindex?html}"<#rt/>
</#if>
<#if parameters.id?exists>
id="${parameters.id?html}"<#rt/>
</#if>
<#if parameters.cssClass?exists>
class="${parameters.cssClass?html}"<#rt/>
</#if>
<#if parameters.cssStyle?exists>
style="${parameters.cssStyle?html}"<#rt/>
</#if>
<#if parameters.title?exists>
title="${parameters.title?html}"<#rt/>
</#if>
<#include "/${parameters.templateDir}/simple/scripting-events.ftl" />
<#include "/${parameters.templateDir}/simple/common-attributes.ftl" />
/>
This template is written in FreeMarker. It starts by emitting the beginning of an input tag of type text on the first line. Then using #if statements it examines the various possible parameters to the tag and outputs the appropriate HTML. Near the bottom it includes two additional scripts: scripting-events.ftl and common-attributes.ftl for handling scripting and attributes that are common to all HTML tags.
This method of building scripts by including others is used pervasively throughout the templates.
The parameters that are passed into the tag via attributes are available using the parameters object. We will use this object to access additional parameters to extend the XHTML theme.
<s:bean name="java.util.HashMap" id="qTableLayout">
<s:param name="tablecolspan" value="%{4}" />
</s:bean>
The qTableLayout is a HashMap and holds one property, tablecolspan, which is the number of columns we want our form to use. The sample above would produce a layout using four columns.
${stack.setValue('#qTableLayout.currentColumnCount', columnCount)}
This statement accesses the OGNL ValueStack and stores the currentColumnCount in our qTableLayout bean. This allows our theme to maintain state between each invocation of the FreeMarker templates that back the Struts 2 tags.
<#--
controlheader-trlogic.ftl
This template handles:
* intializing qTable.currentColumnCount if it has not been initialzed,
* emiting a <tr> tag if the currentColumnCount == 0,
-->
<#if qTableLayout.exists && ! qTableLayout.currentColumnCount?exists >
<#-- Set the currentColumnCount to 0 because this is the first row of the table. -->
${stack.setValue('#qTableLayout.currentColumnCount', 0)}
</#if>
<#-- Do we need to write the opening tr tag. -->
<#if qTableLayout?exists && qTableLayout.tablecolspan?exists >
<#assign columnCount = qTableLayout.currentColumnCount />
<#else>
<#-- Set the currentColumnCount to 0 because this is the first row of the table. -->
${stack.setValue('#qTableLayout.currentColumnCount', 0)}
<#assign columnCount = 0 />
</#if>
<#if columnCount == 0>
<#-- Write out the opening tr tag to start the table row. -->
<tr><#rt/>
</#if>
As you can see in this listing the qTableLayout bean is used to hold the currentColumnCount, which is used to if we need to emit a <tr> tag when the column count equals zero (0). In the controlfooter-trlogic.ftl template if the current column count is equal to or greater than the qTableLayout.tablecolspan the template will emit our closing <tr> tag.
<s:textfield label="Loading Instructions" name="order.dutyPrepaid" size="70">
<s:param name="labelcolspan" value="%{2}" />
<s:param name="inputcolspan" value="%{6}" />
</s:textfield>
Inside our <s:textfield> tag we have nested two <s:param> tags, one to specify the number of columns for our label, and the input. This allows us great flexibility in laying out our forms.
Out theme also supports simply out putting an input without a label. Just omit the label from your tag:
<s:textfield name="someProperty" />>This will emit a <td> containing an HTML input only. No label will be generated. This allows you to do tabular inputs very simply as follows:
<s:textfield name="propertyOne" />> <s:textfield name="propertyTwo" />> <s:textfield name="propertyThree" />> <s:textfield name="propertyFour" />>If you configured your form to have four (4) columns this would result in one row. Wrap this in an <s:iterator> tag and you can quickly create tabular input screens for updating large numbers of records.
<%-- Setup the number of columns to be in the table layout for the form. --%>
<s:bean name="java.util.HashMap" id="qTableLayout">
<s:param name="tablecolspan" value="%{8}" />
</s:bean>
<s:form action="complexForm.action" method="post" theme="qxhtml">
<s:textfield label="SO Number" name="order.fromPartyOrderNumber">
<s:param name="labelcolspan" value="%{2}" />
<s:param name="inputcolspan" value="%{2}" />
</s:textfield>
<s:select name="salespersonId" label="Salesperson" list="genericList" listKey="id" listValue="name">
<s:param name="labelcolspan" value="%{2}" />
<s:param name="inputcolspan" value="%{2}" />
</s:select>
<s:textfield label="Trucker" name="order.dutyPrepaid" size="70">
<s:param name="labelcolspan" value="%{2}" />
<s:param name="inputcolspan" value="%{6}" />
</s:textfield>
<s:select name="newPoRequired" label="New PO Required" list="genericList"
listKey="id" listValue="name" />
<s:select name="shipdateConfirmed" label="Ship Date Confirmed"
list="genericList" listKey="id" listValue="name" />
<s:select name="accounting90EntryDone" label="Acctg Entry Done"
list="genericList" listKey="id" listValue="name" />
<s:select name="factored" label="Factored" list="genericList" listKey="id"
listValue="name" />
<tr><th align="center" colspan="8">Line Items</th></tr>
<tr>
<th>#</th>
<th>Product</th>
<th>Qty</th>
<th>Unit Price</th>
<th>Allocation Instructions</th>
<th>Label Instructions</th>
<th>Description</th>
<th>Override Reason</th>
</tr>
<s:iterator value="lineItems" status="status">
<s:component template="/components/textcell.ftl" value="%{#status.index}" />
<s:select name="termsId" list="genericList" listKey="id" listValue="name" />
<s:textfield name="test" size="1"/>
<s:textfield name="test" size="1"/>
<s:textfield name="test" size="1"/>
<s:textfield name="test" size="1"/>
<s:textfield name="test" size="1"/>
<s:textfield name="test" size="1"/>
</s:iterator>
<s:submit value="Create Sales Order" align="center">
<s:param name="colspan" value="%{8}" />
<s:param name="align" value="%{'center'}" />
</s:submit>
</s:form>
This sample shows the full power of the theme at this point. It uses variable label and input column spans, textfield without labels, and a custom component for outputting text in a table data, but including it in the theme and counting columns so the table rows are started appropriately. (Look at the first tag inside the iterator for the component.)
Here's what it looks like:
<s:textfield label="City" name="city" /> <s:textfield label="State" name="state" /> <tr> <td>Zip+4</td> <!-- Manually outputting our label --> <td> <s:textfield name="zip" theme="simple" /> <s:textfield name="plus4" theme="simple" /> </td> </tr>Although this is serviceable, it would be better if we could allow the Struts 2 tags to handle our layout. It simplifies form creation and allows uniform styling to be automatically applied. Also, should a change happen in how zip codes are collected, we would have to revisit every form that has this HTML construct on it, and change it. If we were to replace this with a component, all of our markup for zip+4 would be contained in one place in our code, and could be changed simply by changing our component template.
1: <#include "/${parameters.templateDir}/${parameters.theme}/controlheader.ftl" />
2: <#include "/${parameters.templateDir}/simple/text.ftl" />
3: <#include "/${parameters.templateDir}/xhtml/controlfooter.ftl" />
First let's examine this template:
<#include "/${parameters.templateDir}/${parameters.theme}/controlheader.ftl" />
<@s.textfield theme="simple" name="${parameters.zipName?default('')}"
value="${parameters.zipValue?default('')}" /> +
<@s.textfield theme="simple" name="${parameters.plus4Name?default('')}"
value="${parameters.plus4Value?default('')}" />
<#include "/${parameters.templateDir}/xhtml/controlfooter.ftl" />
Using the existing Struts 2 textfield allows us to leverage the existing tags. This allows a markup to remain highly consistent.
Accessing the information passed into our component is done using the parameters variable. Our component will take four values:
<s:textfield name="" label="City" />
<s:textfield name="" label="State" />
<s:component template="/components/zipplus4field.ftl" label="Zip+4">
<s:param name="zipName" value="%{'zip'}" />
<s:param name="zipValue" value="%{zip}" />
<s:param name="plus4Name" value="%{'plus4'}" />
<s:param name="plus4Value" value="%{plus4}" />
</s:component>
All of the boilerplate HTML code is gone. Instead we are left with very clean Struts 2 tags.
package net.vitarara.quadran.core.test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.ApplicationContext;
class SpringContextTestFactory {
private static final jpaContext
private static final datasourceContext
public static ApplicationContext getJpaContext () {
if (jpaContext == null) {
jpaContext = new ClassPathXmlApplicationContext
("classpath*:applicationContext.xml");
}
return jpaContext
}
public static ApplicationContext getDatasourceContext () {
if (datasourceContext == null) {
datasourceContext = new ClassPathXmlApplicationContext
("classpath*:test-applicationContext.xml");
}
return datasourceContext
}
}
Notice that is written in Groovy. I'm using Groovy for all of my test cases. The same principles would apply to a factory class written in Java. I have two potential application contexts, a simple one that only has a datasource, and my full one supporting my JPA environment, and all of my Struts 2 action beans, DAO's and service beans. In other words my whole application environment.
Here's a sample test of a DAO:
package net.vitarara.quadran.core.data.jpa;
import net.vitarara.quadran.core.data.*;
import net.vitarara.quadran.core.test.SpringContextTestFactory;
import org.springframework.context.ApplicationContext;
class ShipmentDaoITest extends GroovyTestCase {
void testFindForOutstandingOblReport () {
def dao = SpringContextTestFactory.getJpaContext().getBean ("shipmentDao");
def shipments = dao.findForOutstandingOblReport ();
assertTrue ("Issuficient records returned", shipments.size > 20);
}
}
I simply use my factory to get a handle to my Spring ApplicationContext, and use it to get a "shipmentDao." Then I can call any methods on it I like, testing the results.
Testing a Struts 2 Action is substantially the same.
package net.vitarara.quadran.core.web.shipping
import net.vitarara.quadran.core.test.SpringContextTestFactory;
class ListShipmentsITest extends GroovyTestCase {
void testFindByOrderNumber () {
def action = SpringContextTestFactory.getJpaContext().getBean
("listShipmentsAction");
def model = new ListShipmentsModel ()
model.orderNumber = "SI142784"
action.model = model
action.execute ()
assertTrue ("No shipments were found", action.shipments.size > 0)
}
}
I use my SpringContextTestFactory to get a handle to my ApplicationContext, and get a "listShipmentsAction" bean. The listShipmentsAction beans has a dependency on a ShippingManager service bean, which has a dependency on a ShipmentDao bean, which has a dependency on a JPA EntityManagerFactory. All of these dependencies are injected by Spring, giving me an action I can test against my database.
I instantiate a ListShipmentsModel, which holds my query parameters, plug in a value, set the model on the action, and call the action's execute method. Once the action has executed I can check the results any way I'd like. In this case there is a list "shipments" that is a member of the action, I check to be sure that its size is greater than 0.
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<configuration>
<tasks>
<taskdef name="groovyc"
classname="org.codehaus.groovy.ant.Groovyc">
<classpath refid="maven.compile.classpath"/>
</taskdef>
<mkdir dir="${project.build.outputDirectory}"/>
<groovyc destdir="${project.build.outputDirectory}"
srcdir="${basedir}/src/main/java/"
listfiles="true">
<classpath refid="maven.compile.classpath"/>
</groovyc>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<configuration>
<tasks>
<taskdef name="groovyc"
classname="org.codehaus.groovy.ant.Groovyc">
<classpath refid="maven.compile.classpath"/>
</taskdef>
<mkdir dir="${project.build.testOutputDirectory}"/>
<groovyc destdir="${project.build.testOutputDirectory}"
srcdir="${basedir}/src/test/java/"
listfiles="true">
<classpath refid="maven.test.classpath"/>
</groovyc>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
Now we need to create a profile that tells Maven to run our itegration tests.
<profiles>
<profile>
<id>itest</id>
<activation>
<property>
<name>itest</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>surefire-it</id>
<phase>integration-test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<excludes>
<exclude>none</exclude>
</excludes>
<includes>
<include>**/*ITest.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
This profile attaches itself to the integration test phase and only runs test that end in ITest. (I'm not sure why the include uses **/*ITest.java, but it works.) If you already have profiles configured leave off the profiles tags.
Then we need to exclude the integration tests from our unit test run. This snipped goes in your <build><plugins> section.
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/*ITest.java</exclude>
</excludes>
</configuration>
</plugin>
The final issue you need to take care of is speficying the correct xerces dependency. Add the following to your dependencies:
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.8.1</version>
<scope>test</scope>
</dependency>
That tells Maven to use Xerces 2.8.1 when running tests.
I place my Groovy test files in src/test/java. I don't have a separate directory for them. You could do that by configuring the Groovy compilation to look elsewhere very easily. I haven't done it because all of my tests are in Groovy, and well, I'm lazy.
<action name="createSalesOrderConfirmation" class="sales.CreateSalesOrderAction">
<result name="redirect" type="redirect-action">
<param name="actionName">displaySalesOrder</param>
<param name="namespace">/order/sales</param>
</result>
</action>
This is fairly straight forward. The type of the result is "redirect-action". You set parameters on the result using the param tags. In this case you set the actionName to displaySalesOrder, and the namespace to "/order/sales".
<action name="createSalesOrderConfirmation" class="sales.CreateSalesOrderAction">
<result name="redirect" type="redirect-action">
<param name="actionName">displaySalesOrder</param>
<param name="namespace">/order/sales</param>
<param name="parse">true</param>
<param name="id">${order.id}</param>
</result>
</action>
In this redirect-action result we've added two parameters using param tags. The parse parameter tells Struts 2 to parse any additional parameters using the OGNL expression language. The "id" parameter tells Struts 2 to add this as a parameter to the next action by evaluating the OGNL statement against the current value stack.
In this case my current action, not the one I'm forward to, has an order property, which has an id property. This statement will add the value of this property to the redirect resulting in a forwarding URL like this:
http://www.mydomain.com/mycontext/order/sales/displaySalesOrder.action?id=123You can set additional parameters on the redirect, and have them encoded using the "encode" parameter, or set the method using the "method" parameter.
public class MyEntity {
private String id, value;
public String getId () { return id; }
public void setId (String in) { id = in; }
public String getValue () { return value; }
public void setValue (String in) { value = in; }
}
This is a simple JavaBean that contains two properties id and value.
public class EditMyEntitiesAction extends ActionSupport implements Preparable {
// Service Beans
MyEntityDao myEntityDao = new MyEntityDao (); // A data access object for managing MyEntity objects.
// Model Beans
List<MyEntity> myEntities;
Map<String,MyEntity> myEntitiesMap;
public void prepare () {
// Get the List of MyEntity objects from the datastore.
this.setMyEntities (myEntityDao.findAll () );
// Create a Map using this.myEntities as the basis for it keyed on myEntity.id.
Map<String,MyEntity> this.myEntitiesMap = new HashMap ();
for (MyEntity myEntity : this.getMyEntities () ) {
this.myEntitiesMap.put (myEntity.getId (), myEntity);
}
}
public String execute () throws Exception {
// Iterate over the List of MyEntity objects and persist them using our DAO
for (MyEntity myEntity : this.getMyEntities () ) {
this.myEntityDao.updateDatabase (myEntity);
}
}
/* Accessors */
public void setMyEntities (List<MyEntity> in) {
this.myEntities = in;
}
public List<MyEntity> getMyEntities () {
return this.myEntities;
}
public Map<String,MyEntity> getMyEntitiesMap () {
return this.myEntitiesMap;
}
}
That's all there is to the action. In the prepare we get the List of MyEntity objects and then put them into a map keyed on MyEntity.id. Pretty straight forward. The Map is the key to the whole process. We will use the OGNL EL notation to access that Map and fill the values back into it when a user submits the form.
Here's the pertinent section of the JSP:
<s:iterator value="myEntities">
<s:textfield name="myEntitiesMap['%{id}'].value" value="%{value}" />
</s:iterator>
| Expression | Result |
|---|---|
| myEntitiesMap | This will call getMyEntitiesMap() on our EditMyEntitiesAction. |
| ['1'] | This will call get("1") on the Map that was returned by getMyEntitiesMap(), which will return a MyEntity object. |
| .value | Will call setValue() on the MyEntity object returned previously. Setting the value of the property to the value that was submitted by our user. |
package org.apache.struts2.interceptor;
public interface ConversationScopeAware {
public static final String S2_CONVERSATION_SCOPE = "S2_CONVERSATION_SCOPE";
public static final String S2_CONVERSATION_ID = "S2_CONVERSATION_ID";
public static final String S2_VALUE_STACK_MODEL_KEY = "s2cmodel";
public static final String S2_VALUE_STACK_MODEL_ID_KEY = "modelid";
/** Get the id of the current conversation the action is handling. */
public String getConversationId ();
/** Set the id of the current conversation the action is handling. */
public void setConversationId (String in);
/** Get the conversation model. */
public Object getConversationModel ();
/** Set the conversation model. */
public void setConversationModel (Object in);
/**
* Prepare the conversationModel. So it can be saved in the conversationScope and
* pushed onto the top of the value stack. The action needs to maintain a reference
* to the prepared model.
*/
public Object prepareConversationModel ();
/**
* Find out if the conversation is finished. If true the conversation model can
* be removed from the conversationScope.
*/
public boolean isConversationFinished ();
}
The most important concept to grasp is the conversationModel. This is very similar to the ModelDriven actions. Basically you have some Object that represents the data you want to collect and process as you have a conversation with your user. Your model object can be almost anything, including your rich domain objects. I would discourage you from using a Map though.
When Struts 2 receives a request for a ConversationScopeAware action it will check to see if there is a stored conversation using the conversation id. If there is no conversation it will ask the Action to prepare the conversation model by calling ConversationScopeAware#prepareConversationModel(). This is your chance to setup your data model that you will fill in over your conversation. Once you have prepared your model the interceptor will push it onto the top of the stack, so the properties can be filled in with the user's parameters.
In your view you will need to include the conversation id in a hidden field of your form:
<s:hidden name="S2_CONVERSATION_ID" value="%{conversationId}" />
Once you have finished with the conversation you can mark it for removal from the users conversation scope. After running the action the interceptor will call isConversationFinished(). If this returns true it will dispose of the saved conversation model.