Vita Rara: A Life Uncommon

Creating a Struts 2 Action in Groovy


Categories: | | | |

(N.B.: For the latest on Struts 2 and Groovy see: Groovy Works)

Actions in Struts 2 are POJO's. Actions also serve the same purpose of the ActionForm from Struts 1, storing all of the data submitted by your user. Additionally Struts 2 action classes store the information you wish to display back to your user. (NB: Struts 2 also supports a ModelDriven style, where the Action does not need to store this data, but the ideas are largely the same.)

Over the course of developing a Struts 2 application you will write a lot of accessors, setThis(String in), getThat(). Personally I'm sick of writing those methods. Yes, I know Eclipse could write them for me, but I don't like Eclipse, and I haven't taken the time to learn another IDE. I like my Unix shell, maven, screen and vi (vim), thank you very much.

So, what's a guy to to? Groovy Beans! Groovy Beans are a way of writing classes in Groovy that comply with the Java Beans spec. Nice this is they are a lot shorter.

An example:


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.

Dynamically loaded action

Hello I tried to create dynamically loaded Action writen in Groovy.
I created simple Groovy Action.

class SimpleGroovyAction extends ActionSupport{
	TestInterface groovyTest;

	public String getMessage(){
		return "I am the Actionnnnnnnnnnn";
	}
}

I can load it dynamically into Spring Context

< lang:groovy 
	id="groovyTest"
        scope="prototype"
	script-source="classpath:edu/pw/imio/groovy/SimpleGroovyAction.groovy"
	refresh-check-delay="1000">
< /lang:groovy >


Then Struts can load it by id

<action name="simpleGroovyAction" class="groovyTest" >
	<result>/WEB-INF/pages/groovyAction.jsp</result>
</action>

And it all works
But I can't access getMessage via ONGL. Do enybody know what is the problem
Sorry for &lt &gt

Finally I used Groovy Works But this plugin offers different aproach

OGNL Access

How did you try to access it with OGNL? In a JSP?

I have properties that work like that all over the place in my actions that we access from JSP's as follows:

<s:property value="message" />

Mark

Just wondering

Mark,

Just wondering; why don't you rely on OGNL to instantiate the VRContext object ? I believe OGNL can also set fields (but I'm not sure it's enabled in S2). Wouldn't you agree that providing getters and setters on the VRContext for exactly this purpose is a good idea ?
Is there a reason why you're not relying on the XWork validation ? It would give you a lot more control, and it's completely seperated from your Action.

If you apply both tips, you'll find your Action to be another 40 lines (if not more) shorter, and a lot cleaner (of course, you'll have to create an additional validation file ..).

Cheers,

Phil

Limited Time

Hi Phil,

I can't say as I follow you regarding using OGNL to populate the VRContext. The purpose of the VRContext is just to have an object to pass between the controller and the various service beans that will hold multiple objects. This keeps me from having to change method signatures if I need to add some argument to the call. Also the VRContext is just a Map, with some extra methods. I have had bad luck using OGNL to set values on Maps from user input. Everything ends up being a String[].

As to XWork validation, it just limited time. I've been learning Struts 2 as I go, and have been adding various portions of its functionality to my development process over time. I have been planning on looking at the validation framework. I guess it's time. One of the reasons I haven't looked closely at it, is I thought it was mainly field based, rather than semantic. I'll have to look into it.

Thanks for the comment,

Mark

Re: Limited Time

What I actually meant was to have a getter and setter for your VRContext, and let OGNL set the properties for you (be it that there might be some limitations). What do you mean by "it's just a map" ? Do you mean you have a Map property in your VRContext object ? Or do you extend it ? If you extend it, you might have to rely on type conversion to get the values set correctly - I know, this can be tricky.

Well, the validation can be field based - but you can just as well do any validation you want (eg. checking if a user isn't posting twice in 10 seconds, see if a user belongs to a certain user group, or check if the weather forecast is 'sunny and hot' - you get the point), except these last checks will be actionErrors rather than fieldErrors.

Using Groovy actions with S2 is a very good idea, and something I plan on doing as well. Keep those Struts 2 entries coming ! ;-)

Maps and Struts 2

Hi Phil,

I understand what you meant now. I tried using the VRContext, which extends HashMap, as a property of my action and letting OGNL fill in the values. The problem I ran into was that all of the properties ended up being String[]. The type conversion didn't work. I think there is a way to tell OGNL what the type of a property should be, but just using a first class bean seemed easier.

I've simplified the marshaling of the data to and from the VRContext using a helper method that copies all properties of a bean to a Map. That was a simple two liner in Groovy.

Mark