Sprout: Annotation-Powered Simplicity for Struts

Overview

Sprout aims to significantly simplify development with Struts by reducing the amount of configuration required through the use of annotations and sensible defaults.

Basics

Sprout requires JDK 1.5. Without that, you're out of luck.

Sprout is an extension of a Struts MappingDispatchAction, which allows for multiple actions to be defined within the same Action class. In this case, the name of the method is mapped to the URL (after converting method names; methodName is exposed as /method\name.do). Paths are determined based on the package name of the action. For example, _net.mojodna.sprout.action.HomeAction corresponds to /, while net.mojodna.sprout.action.example.ExampleAction corresponds to /example/.

Sprout also uses a custom RequestProcessor--SproutRequestProcessor, which extends Spring's DelegatingRequestProcessor. This means that you can specify dependencies within your actions using setter-injection.

Sprout is also completely backward-compatible with legacy Struts applications.. It was built for use in a legacy Struts application; many of the older Actions are untouched--new development is done using Sprout (I often find myself removing action-mappings while adding new functionality).

Sprout is available on GitHub here: http://github.com/mojodna/sprout Do with it what you will.

Annotations

All annotations are optional.

@FormName

Allows the developer to override the name of the form-bean (defined in struts-config.xml) used for this method. This is equivalent to setting the name attribute within an action mapping.

Defaults to ${action-name}Form; e.g. for AdminAction the default ActionForm name would be AdminActionForm.

@Forward

Specifies additional forwards. Multiple forwards may be specified by providing arrays as arguments to name, path, and redirect. redirect defaults to false.

A default redirect is provided; the key is Sprout.FWD_SUCCESS and the path is the converted path + .jsp. E.g., AdminAction.methodName() corresponds to method\name.jsp_.

e.g. @Forward(name="failure", path="/failure.jsp" redirect="true")

@Input

This annotation is required if this action is validating the output from a different action. In that case, the argument to @Input should be the path to the JSP containing the form whose input is being validated.

e.g @Input("login.jsp") if this is not login() and the action that initiated this request is login().

@Scope

Specifies the scope attribute for the generated action mapping. This exists primarily for completeness; it is likely that you may never use this annotation.

As with struts-config.xml, the default is request.

@Validate

Specifies the validate attribute for the generated action mapping. Set this to true if you desire the output of this action to be validated. For this to have any effect, you must have specified rules in validator-rules.xml.

Sprout does not contain anything to ease the actual validation process at this time.

Example

src/java/net/mojodna/sprout/action/example/ExampleAction.java:

// URL should be /example/*
package net.mojodna.sprout.action.example;

// ...

public class ExampleAction extends Sprout {
  // overrides Sprout.index()
  public ActionForward index( ... ) {
    // do something

    // redirect to index.jsp
    return mapping.findForward( FWD_SUCCESS );
  }
}

src/java/applicationContext.xml:

...
<bean name="ExampleAction" class="net.mojodna.sprout.action.example.ExampleAction" singleton="true" />
...

src/web/WEB-INF/struts-config.xml:

...
<form-bean name="ExampleActionForm" type="org.apache.struts.validator.DynaValidatorForm">
  <form-property name="..." type="..." />
  ...
</form-bean>

<!-- No action-mappings!!! -->
<action-mappings />

<!-- Define an alternate RequestProcessor -->
<controller processorClass="net.mojodna.sprout.SproutRequestProcessor" />

<!-- Sprout plug-in -->
<plug-in className="net.mojodna.sprout.SproutAutoLoaderPlugIn" />
...

Shorthand

Index Actions

Sprout contains an index() method to speed up the process of getting something working. To use this, subclass Sprout (no methods necessary), register the action-bean in applicationContext.xml and create a corresponding index.jsp. When you need to add logic to the action, override index() in your Sprout sub-class and add it there.

DynaActionForms

Helper methods have been added to ease development using DynaActionForms.

String key = "foo";
String value = "bar";
// returns a String
f( key ) == ((DynaActionForm) form).getString( key );

// returns an Object
F( key ) == ((DynaActionForm) form).get( key );

// sets a value
s( key, value ) == ((DynaActionForm) form).set( key, value );

ActionMessage handling

Sprout contains adaptations to the traditional way Struts handles ActionMessages. getMessages() and getErrors() have been modified to store and retrieve messages from the session rather than the request. This means that messages and errors will be displayed (and subsequently cleared) on the next invocation of <html:messages /> or a variant (such as <ui:notifications />), regardless of whether either of the get methods have been called or if the invocation occurs during a separate request.

Sample message / error handling code (within an Action):

ActionMessages msgs = getMessages( request );
ActionMessages errors = getErrors( request );

// Add a message
msgs.add( ... );

// Add an error
errors.add( ... );

// Save messages and errors
saveMessages( request, msgs );
saveErrors( request, errors );

<ui:notifications /> (src/web/WEB-INF/tags/ui/notifications.tag) is an alternative tag file that can be modified for your use. The primary difference between <html:messages /> is that it will display both messages and errors (and will discriminate between them, allowing you to style them differently depending on your application's needs).

Servlets and Taglibs

Spring's Struts integration lacks the ability to autowire servlets and taglibs. Sprout contains classes (Sproutlet and SproutTag) that can be subclassed to provide auto-wiring capability. They will be wired during their initialization process.

These support classes only support the byName auto-wiring mechanism.

Q + A