Building Struts 2 Apps Without XML Gluecode

Java Development News:

Building Struts 2 Apps Without XML Gluecode

By Ted Husted

01 Jan 2008 | TheServerSide.com

In this article, we jettison XML gluecode for "convention over configuration". Using the SmartURLs plugin for Struts 2, we can autowire Action classes to page templates with search-engine-optimized URIs.

This article covers:

  • Exploiting convention over configuration
  • Eliminating XML gluecode
  • Validating input with annotations

What's XML gluecode?

Success breeds success, and successful applications tend to grow larger and more complex over time. Most applications are so large and so complex that we can't think about the entire application at once. To get through the day, we need to focus our attention on one thing at time.

One way to simplify complex applications is to separate concerns. We group similar tasks together, so that we can focus on one element of the application at a time. Two concerns that we like to separate are presentation logic and business logic. It's easier to design presentation logic using a page template, and it's easier to design business logic using conventional source code. Each concern is easy to manage on its own, but we need a way to put the concerns back together.

In a conventional Struts application, we glue concerns together using declarative statements in an XML document. In practice, we write a lot of XML stanzas that just say: "For this request, run this Java class and then render this page template." "Example 1: struts.xml" shows a bit of XML gluecode.

"Example 1: struts.xml"

<action name="hello-world" class="actions.HelloWorld">
  <result>/results/hello-world.jsp</result>
</action>

At first, a dash of gluecode may seem trivial, but as applications grow in size, we can waste as much time fussing with gluecode as we spend on coding Action classes or writing JSP pages.

Nowadays, a popular way to avoid XML "gluecode" is to use "convention over configuration".

What's convention over configuration?

Instead of wiring components together with configuration files, another strategy is to use consistent naming conventions, and then make the conventions part of the application framework. Many Struts developers already use naming conventions to track which components go together. We just need to program the framework to observe the same kind of conventions that most of us already use.

For example, if a client requests a "hello-world.action", it makes sense for the framework to look for a "HelloWorld.class" and/or a "hello-world.jsp". If the Action class returns "small" as a result code, it makes sense to first look for a "hello-world-small.jsp" (or a "hello-world-small.vm", you prefer Velocity templates). If "hello-world-small.jsp" is not found, then it would also make sense to check for a plain-old "hello-world.jsp".

These matching rules may sound simplistic, but, in practice, a few simple rules is all that we need to write entire Struts applications without even a smidgeon of XML gluecode!

Should our conventions fall short, the SmartURLs plugin also provides annotations that we can use to override the matched action or result, on a case-by-case basis.

Does Struts 2 support convention over configuration by default?

The Struts 2 core offers a few features that can reduce configuration. But to get the full benefit of "convention over configuration", we need to add a new plugin to the mix.

The SmartURLs plugin for Struts 2 (http://cwiki.apache.org/S2PLUGINS/smarturls-plugin.html) offers a full-featured approach to convention over configuration. A request for "/my-action" is automatically mapped to a MyAction class and the result to a my-action.jsp page. To keep the system flexible, heuristic defaults look for alternative matches if MyAction class or my-action.jsp is not present. The matching rules are laid out in "Sidebar 1: SmartURLs Rules"

"Sidebar 1: SmartURLs rules"

URL to Action class (/my/package/hello-world):

  • A-1 Extract the final URL path segment (hello-world).
  • A-2 Upper-case the initial letter, and if any hyphen appears within the URL path, upper-case any following letter, and remove the hyphen (HelloWorld).
  • A-3 Convert the rest of the path to lowercase, and substitute slashes with dots (my.package.).
  • A-4 Check the base action packages for a class matching package + action (actions.my.package.HelloWorld).
  • A-5 If a matching Action is not found, use the package's default Action class (ActionSupport).

Result code to Path:

  • B-1 Append the result-code returned by an Action to original URL path (/my/package/hello-world-success).
  • B-2 Check the base result folders for a matching JSP, Freemarker Template, or Velocity Template (/WEB-INF/results/my/package/hello-world-success.jsp).
  • B-3 If the template is not found, remove the response code, and try again (/WEB-INF/result/my/package/hello-world.jsp).
  • B-4 Raise a standard 404 error is a matching template is not found.

SmartURLs provides features like:

  • Extensionless URIs
  • Automatic binding of action URLs to conventional class and page names
  • Action URI format that is "Search Engine Optimization" (SEO) compliant
  • Annotations to specify a different action name, or even multiple names
  • Automatic support of JSP, Freemarker, and Velocity result types
  • Robust "index" page handling (/products will match actions.Products or actions.products.Index)

How would we write a SmartURLs "Hello World" page?

Frameworks like Struts decompose an application's workflow into a series of actions. Each action might have associated with it several concerns, including input validation, business logic, persistence logic, message resources, text formatting, and an output resource. Each action has its own context, and which concerns are applied to any given action may vary according to circumstance.

The simplest use case for an action is rendering a page template without specifying any other concerns. (Just show me the @%#$^ page!) For our SmartURLs hello world example, let's try the simplest case first and then add in some of the other concerns. "Sidebar 2: Render page template" outlines the use case.

"Sidebar 2: Render page template"

System renders a page template without a custom Action class.

  1. Client requests an action resource.
  2. System determines there is no custom Action class configured for the resource.
  3. System renders page template for the resource using a default Action class.
  4. Client presents HTML version of the page template.

We can configure SmartURLs to expect page resources (JSP, Freemarker, or Velocity) to be found under "WEB-INF/results". This location is accessible to the Struts 2 framework, but it is not directly accessible to a web browser. The page resources cannot be accessed except through Struts 2 (or another server-side component).

Into the "WEB-INF/results" folder, we can drop a simple JSP template, as shown by "Example 2: hello-world.jsp".

"Example 2: hello-world.jsp"

<html>
 <body>
  <p>
   Hello World!
  </p>
  <p>
   It is now <%= new java.util.Date() %>.
  </p>
 </body>
</html>

With the SmartURLs plugin installed, we can fire up our web container and open the URL "http://localhost:8080/smartapp/hello-world". (The hostname and port for your container may vary!)

In response, SmartURLs will render the "WEB-INF/results/hello-world.jsp" template, as shown in "Figure 1: Hello World!"

"Figure 1: Hello World!"

If the page renders, we know that SmartURLs is working!

Already, SmartURLs lets us

  • use extensionless URIs,
  • store templates under the secure WEB-INF folder, and
  • do without "gluecode" XML

How would we display our own data on the page?

As handy as checking the server time may be, we usually want to merge our own data into a page template. Typically, we want to set a property on a Struts Action and then display that property in the page.

The value of the property might come from a database, and be filtered through a set of business rules, but the page doesn't need to know that. All the page needs to know is that the property is available, and it's the page's job to display the value, whatever it is. Conversely, the Action class doesn't need to know anything about HTML or expression languages. It just sets the property.

Right now, our concern is whether the Action class and page template are automatically wired together by the framework. So all we really need to do is set an Action property to a known value, and see if the page displays the value we expect.

To test SmartURL's autowiring, let's add a HelloWorld class to an "actions" package in our application, as show in "Example 3: HelloWorldAction.java".

"Example 3: HelloWorldAction.java"

package actions;
public class HelloWorldAction  {
  private String greeting;
  public String getGreeting() {
    return greeting;
  }
  public String execute() {
    greeting = "The server time is " + new java.util.Date().toString();
    return "success";
  }
}

Notice that this is a "POJO" Action class (a standard Struts 2 feature). We don't need any of the framework's special services, so we didn't extend a base class or implement a special interface. The closest thing to a special feature is the execute method! One other concession is that we need to append "Action" as a suffix to the classname.

Also note that we changed the message slightly, so that we can be sure that our page is updated. The updated page is shown in "Example 4: hello-world.jsp (2)"

"Example 4: hello-world.jsp (2)"

<html>
 <body>
  <p>
   Hello World!
  </p>
  <p>
   ${greeting}
  </p>
 </body>
</html>

Of course, we could use a Struts 2 tag instead of the JSTL expression, but <s:property value="greeting"/> seems verbose compared to "${greeting]". Figure 2: "Hello World! (2)" shows the updated page.

"Figure 2: Hello World! (2)"

Can SmartURLs handle a data-entry workflow with input validation?

A strength of action frameworks, like Struts 2, is that, depending on the runtime circumstances, an action can change the workflow by selecting different output pages. For example, we might want to collect input on one page. If the input is correct, we might want to go to an output page. If the input is not correct, we might want return to the input page.

Since validation workflow is one of the special services that the framework provides, in "Example 5: HelloWorld.java (2)", we extend our class from the ActionSupport class.

"Example 5: HelloWorld.java (2)"

package actions;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.validator.annotations.*;
@Validation()
public class HelloWorld extends ActionSupport {
  private String greeting;
  @RequiredStringValidator(message="Please enter a greeting!")
  public String getGreeting() {
    return greeting;
  }
  public void setGreeting(String value) {
    greeting = value;
  }
}

Since we now extend the Action interface, the SmartURLs rules also lets us drop the "Action" suffix from the classname.

In Example 5, we use validation by annotation (another standard Struts 2 feature) to ensure that a message is entered. (Annotations, introduced in Java 5, are specified in the program source by using the @ symbol.) Struts 2 and the SmartURLs plugin use annotations as an alternative to XML gluecode. In Example 5, we use an annotation to "glue" a RequiredStringValidator to the message property. The core framework provides annotations for all the usual validators.

To collect an input message to validate, we can add another page template, as shown in "Example 6: hello-world-input.jsp".

"Example 6: hello-world-input.jsp"

<%@ taglib uri="/struts-tags" prefix="s" %>
<html>
 <body>
  <p>
   What would you like to say to the world?
  </p>
  <s:form action="hello-world">
    <s:textfield label="Greeting" name="greeting" />
    <s:submit />
  </s:form>
 </body>
</html>

"Figure 3: Hello World Input!" shows the page as it would be rendered.

"Figure 3: Hello World Input!"

If the message field is left blank, validation will fail, the HelloWorld Action will return an "input" result code, and SmartURLs will forward control back to "hello-world-input.jsp", as shown by "Figure 4: Hello World Input! (2)".

"Figure 4: Hello World Input! (2)"

If we manage to pass validation, a page renders like the one shown in "Example 5: Howdy".

"Figure 5: Hello World Input! (3)!"

Note that we can implement this common workflow without any XML gluecode! In this workflow, the only metadata of any kind is the RequiredStringValidator annotation. The rest is driven by convention over configuration!

What if we wanted to redirect after submitting a form?

If a form passes validation, and we use it to update a database, we often want to "redirect" to a confirmation page. A redirect updates the page location in the web browser's address bar. Among other things, a redirect ensures that people do not resubmit the form again by pressing refresh.

Struts 2 supports redirects through the @Result annotation. To redirect to another location on "success", we can add a "@Result" annotation to the Action class. "Example 7: HelloWorld.java (3)" shows the @Result annotation.

"Example 7: HelloWorld.java (3)"

@Result(name="success", type="redirect", location="hello-world-view")
public class HelloWorld extends ActionSupport

The annotation in Example 7 implies that there is a hello-world-view action. To create one, all we need to do is rename hello-world.jsp to hello-world-view.jsp.

On success, the system will redirect from the Hello Action to "hello-world-view.jsp", and display the greeting we entered through the "hello-world-input" action.

Or not.

By default, the Action properties are retained in request scope. If we redirect, we lose the first request scope and create a second. For now, the simplest solution is to keep our greeting in session scope. The complete, updated class is shown in "Example 8: HelloWorld.java (4)".

"Example 8: HelloWorld.java (4)"

package actions;
import java.util.Map;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.validator.annotations.*;
import org.apache.struts2.interceptor.SessionAware;
import org.texturemedia.smarturls.Result;
@Validation()
@Result(name="success", type="redirect-action", location="hello-world-view")
public class HelloWorld extends ActionSupport implements SessionAware {
    private Map<String, String> session;
    public void setSession(Map value) {
        session = value;
    }
    protected Map<String, String> getSession() {
        Map<String, String> value = session;
        return value;
    }
    public static String GREETING_KEY = "greeting";
    @RequiredStringValidator(message="Please enter a greeting!")
    public String getGreeting() {
        return (String) getSession().get(GREETING_KEY);
    }
    public void setGreeting(String value) {
        getSession().put(GREETING_KEY,value);
    }
}

Now, to enter a greeting we can open "hello-world-input", which submits to "hello-world". If we successfully enter a greeting, "hello-world" stores the property in session scope, and redirects to "hello-world-view". To complete the loop, we can add a link back to the data-entry page. "Example 9: hello-world-view.jsp" shows the updated page.

"Example 9: hello-world-view.jsp"

<html>
 <body>
  <p>
   Hello World!
  </p>
  <p>
   ${greeting}
  </p>
  <p>
  <a href="hello-world-input.do">Try again!</a>
  </p>
 </body>
</html>

At this point, our complete Hello World data-entry workflow is shown by

  • "Example 6: hello-world-input.jsp",
  • "Example 7: HelloWorld.java (3)", and
  • "Example 4: hello-world-view.jsp"

without a single line of XML gluecode!

How do we install the SmartURLs plugin?

To install the plugin, first add the SmartURLs JAR to the web application's lib folder, along with the other Struts JARs. To get the most benefit from SmartURLs, we should make two small changes to the configuration. Among other things, these changes will let us use extensionless URLs.

Second, replace the standard Struts filter with the SmartURLs version. "Example web.xml" shows the SmartURLs <filter> and <filter-mapping> stanzas.

"Example 8: web.xml"

    <filter>
        <filter-name>
            struts2
        </filter-name>
        <filter-class>
            org.texturemedia.smarturls.SmartURLsFilter
        </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>
            struts2
        </filter-name>
        <url-pattern>
            /*
        </url-pattern>
    </filter-mapping>

Third, in the application's struts.xml, add the "constant" stanza shown in "Example 9: struts.xml".

"Example 9: 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">
    <constant name="struts.devMode" value="false" />
    <constant name="struts.action.extension" value="" />
    <constant name="smarturls.action.packages" value="actions" />
    <constant name="smarturls.base.result.location" value="/WEB-INF/results/" ></constant>
    <constant name="struts.custom.i18n.resources" value="support.package" ></constant>
</struts>

A larger application may need to define some other one-time settings or package defaults. But not gluecode!

What's the minimum installation we need for a SmartURLs application?

If you are using MyEclipse or Eclipse with Web Tools (WTP), you can create a Dynamic Web Application, and then

  1. Drop the usual Struts 2 JARs (freemarker, ognl, struts2-core, xwork2) under the standard "lib" folder, along with the SmartURLs plugin JARs (java-net-commons and smarturls-s2).
  2. Under "WebContent/WEB-INF", create a "results" folder for page templates.
  3. Under the "src" folder, create a "struts.xml" based on Example 9, and a "actions" package for Action classes.

And you are ready to go!

Is that all there is?

We've walked through the simplest way to use SmartURLs. We've shown the simplest approach that requires the fewest annotations. Aside from the annotations we've shown, SmartURLs supports several others. For example, we can give an Action multiple names, or call methods other than execute.

The SmartURLs documentation details the various annotations and configuration options. For more of SmartURLS in action, a complete SmartURLs MailReader example application is also available as a ready-to-deploy WAR (check Resources at the end of the article).

Using SmartURLs conventions, we can write less code and still create powerful, enterprise-ready applications. We can then put the time we save toward improving the user interface and other parts of the application that make it more useful.

Today, SmartURLs is a third-party plugin, but work is underway to merge it with the standard CodeBehind Plugin for Struts 2.1. The CodeBehind plugin for Struts 2.0 offers similar capabilities, but fewer features.

Resources

  • SmartURLs plugin site (http://code.google.com/p/smarturls-s2/)
  • SmartURLs MailReader Example for Java 5 (http://husted.com/smart-urls/smart-urls.war)
  • Apache Struts site (http://struts.apache.org)

Biography

Ted Husted is a member of the Apache Software Foundation, an active member of the Apache Struts and Apache iBATIS Projects, and co-founder of the Apache Jakarta Commons. His books include JUnit in Action, Struts in Action, and Professional JSP Site Design. Ted has consulted with teams throughout the United States, including CitiGroup, Wells Fargo, and Pepsi Bottling Group. He provides onsite training to software development teams through his Struts Mentor site (www.StrutsMentor.com). This is the second in a series of articles about writing applications with Struts 2.