In the previous chapter, we had a crash course on how to configure our Struts 2 application and got a (very) small taste of Struts 2 actions—the place where our application does its work. Even if the bulk of our application's functionality resides in service objects, actions are where the service objects are instantiated and used.
In this chapter, we'll examine Struts 2 actions further. While the mantra of "actions are POJOs" echoes in our heads, extending the ActionSupport
class provides us with quite a bit of functionality, including internationalization (I18N), validation, and so on.
Much of this functionality is provided by a combination of interface implementations and interceptors. We'll cover ActionSupport
, a utility class implementing most of the commonly used interfaces, as well as a few other handy interfaces. We'll see how we can get a lot out of the framework, without covering how some of the functionality is actually implemented.
We'll implement a prototype of our first user story, entering a recipe. We'll also take a quick detour into some ways of exploring code, including the usefulness of Unix-like command line utilities, and how the ability to create ad hoc tools can really be handy.
ActionSupport
is an XWork class that provides default implementations of validation and I18N interfaces. Since many actions require both, it makes a convenient "default" action base class. It's not required for our actions to subclass ActionSupport
, but it's handy.
ActionSupport
implements the following six interfaces:
Action
Validateable
ValidationAware
TextProvider
LocaleProvider
Serializable
We'll take a brief look at each of these except Serializable
(it's boring, and is already familiar to Java developers).
The Action
interface defines a single method:
public String execute();
One benefit of implementing Action
is that reflection isn't required to check for the execute()
method. We can just cast to Action
and call execute()
.
ActionSupport
, via the Action
interface, exposes several typical result name strings. For example, we've already seen actions returning "success" and "error". ActionSupport
defines these as SUCCESS
and ERROR
respectively. It also defines INPUT, LOGIN
, and NONE
. Each of these has the value we'd expect.
NONE
is a bit different from the rest. It is used when no view is shown by the framework, as when we write directly to the response.
I18N support is provided by the TextProvider
implementation along with the LocaleProvider
(discussed later in this chapter). The TextProvider
interface provides access to standard Java ResourceBundles
and the messages defined in them. Ultimately, TextProvider
provides two methods:
Both the listed methods have several method signatures. We'll focus on the many variants of getText()
.
The getText()
method comes in several flavors. The simplest is the String getText(String key)method
, which retrieves the named message from a resource file. Messages are retrieved in the hierarchical fashion. Messages are retrieved in the following order:
${actionClassName}.properties
.
${baseClass(es)}.properties
.
${interface(es)}.properties
.
ModelDriven's
model, if action implements ModelDriven
(discussed later). Steps 1-3 are performed for the model class. However, we'll ignore this feature for now.
package.properties
(and any parent package's package.properties)
.
I18N message key hierarchy.
Global resource properties.
To demonstrate the process of locating messages, we'll create the following package and file hierarchy:
/com/packt/s2wad/ch03/i18n/ /com/packt/s2wad/ch03/i18n/package.properties /com/packt/s2wad/ch03/i18n/TextExampleAction.java /com/packt/s2wad/ch03/i18n/TextExampleAction.properties /com/packt/s2wad/ch03/i18n/TextInterface.java /com/packt/s2wad/ch03/i18n/TextInterface.properties /com/packt/s2wad/ch03/i18n/sub/ /com/packt/s2wad/ch03/i18n/sub/package.properties /com/packt/s2wad/ch03/i18n/sub/TextExampleSubAction.java /com/packt/s2wad/ch03/i18n/sub/TextExampleSubAction.properties
We see a Java package containing package.properties
, an action class (TextExamplesAction
), and an interface (TextInterface
). Each has a corresponding property file. TextExamplesAction
extends ActionSupport
and implements TextInterface
. We have a subpackage with its own package.properties
file. The subpackage also contains an action class (TextExamplesSubAction
) extending TextExamplesAction
with its own property file.
Our first sanity check will be simple. We'll test our TextExamplesAction
class by looking up a message from the TextExamplesAction.properties
file. From the list we saw earlier, Struts 2 will look for messages in a property file named after the class.
We'll use the Convention plug-in again, avoiding XML. The action will log the results of a getText()
call. TextExampleAction.properties
contains a single entry:
from.class.props=Hello from TextExamples.properties!
As we're using the Convention plug-in, we need to create a JSP file for our Convention-based action, else Struts 2 will give us an error. For now, we create a dummy JSP file in /WEB-INF/jsps/i18n/text-example.jsp
. The expurgated version (it's the one without the gannet) of our action looks like this:
package com.packt.s2wad.ch03.actions.i18n; public class TextExamplesAction extends ActionSupport implements TextInterface { private static Log log = LogFactory.getLog(TextExamplesAction.class); public String execute() throws Exception { log.debug(getText("from.class.props")); return SUCCESS; } }
When we access our action at /i18n/text-examples
, we should see something resembling the following (assuming we're using the log4j.properties
contained in the chapter's project):
DEBUG com.packt.s2wad.ch03.actions.i18n.TextExamplesAction.execute: 15 - Hello from TextExamples.properties!
If no message is found for a given key, getText()
will return null
.
To avoid going to the console to check our output, let's figure out a way to display our messages on our JSP page. Our first detour serves two purposes:
It provides a very brief introduction to how Struts 2 exposes values to our JSPs
It gives us another acronym to add to our growing collection
Traditionally, accessing application data from our JSP page would involve putting a value into either request or session scope. Some frameworks handle it more automatically, for example, Struts 1 would put an ActionForm
into either request or session scope.
Struts 2 works in a similar fashion. However, Struts 2 simply puts the entire action into scope. (It's actually a bit more complex. The action is actually pushed onto a stack that's in scope, but we'll cover that later.) We then access action properties using Struts 2 custom tags and OGNL (Object Graph Navigation Language), another expression language similar to JSP EL. Another option is to use JSP EL (thanks to Struts 2's custom request wrapper).
Using the Struts 2 custom tags and OGNL gives us another advantage. We can access action methods, including those that take parameters, directly from our JSP. This is in contrast to JSP EL, which allows access only to properties (although access to functions may appear in JEE 6).
Right now, we just want to display localized messages on our JSP page. As mentioned earlier, we can call action methods from our JSP using Struts 2 tags and OGNL. One of the custom tags is <s:property>
, which is the most basic way to access action properties (and methods). We specify the action property (or method) with the value
attribute.
Our stub JSP, which is no longer a stub, will contain the following (the remaining examples in this file will just show the relevant portions):
<%@ taglib prefix="s" uri="/struts-tags" %> <html> <body> <dl> <dt>From TextExamplesAction.properties via property tag, using getText(...):</dt> <dd> <s:property value="getText('from.class.props')"/> </dd> </dl> </body> </html>
This JSP will produce output similar to our console output, but in the browser:
This is a little unwieldy. Being lazy programmers, we want to minimize typing as much as possible. As we have access to action properties as well as methods, we could simply put the results of the getText()
call into an action property.
Action properties are exposed via public getter methods:
public String getText1() { return getText("from.class.props"); }
Now, we can access the text in our JSP via the text1
action property:
<s:property value="text1"/>
Struts 2.1 doesn't actually require a public getter method if we declare the property itself public. OGNL will access public properties without a getter. Whether or not this is a good practice is a debatable issue. The biggest drawback of not following the JavaBean specification is that not all tools or libraries will be as forgiving as OGNL.
Finally, as mentioned, we can access the property via JSP EL. This is achieved using the simplified property access notation introduced in JSP 2.0, reducing the JSP code to a terse:
${text1}
Struts 2 wraps requests in a StrutsRequestWrapper
. This class is responsible for accessing scoped attributes. StrutsRequestWrapper
will check for properties on the Struts 2 value stack first. If no property is found in the stack, it will defer to the default request wrapper, which searches the normal JEE scopes.
Even with the JSP EL shortcut, this is still a fair amount of work, and it doesn't seem reasonable that we have to write Java code just to look up a message. Struts 2 provides an<s:text>
tag, meaning more work in our JSP, but less in our Java.
<s:text name="from.class.props"/>
To protect against missing messages, we can use<s:text>
tag's body to provide a default message value:
<s:text name="an.unlikely.message.name"> Default message if none found </s:text>
This is similar to the functionality provided by the getText(String, String)
method.
Which method we use is largely a matter of preference. The<s:text>
tag is arguably the cleanest, as it's short and it's all in one file.
Next on the list of places to look for messages are the superclass property files. Our TextExampleSubAction
class extends TextExampleAction
. Hence, messages in TextExampleAction.properties
are available to TextExampleSubAction
. Messages defined in both files will resolve to the more specific, giving us the ability to override messages for subclasses, just like we can override class functionality.
To demonstrate, we'll add a message under the key overridden.message
to both TextExampleAction.properties
and TextExampleSubAction.properties
. However, the values will be different (you can guess which file contains the following).
overridden.message=I am from TextExampleSub.properties
We'll add a property and value for the same overridden key to TextExampleAction.properties
, only in the superclass property file:
overridden.message=I am from TextExample.properties superclass.message=Who's your superclass?
We'll add some<s:text>
tags to a new JSP for the subclass, and to our superclass JSP:
<s:text name="overridden.message"/> <s:text name="superclass.message"/>
When we visit /i18n/sub/text-examples-sub
, we'll see the overridden message from the subclass property file, and also the message defined in the superclass's property file.
Messages may also be placed in interface property files, following class and package hierarchy. Similarly, messages placed in a Java package's package.properties
file are available to any class contained in that package. What might be slightly less obvious is that Struts 2 will continue searching up the package hierarchy for package.properties
files until either the message is found or Struts 2 runs out of packages to search.
If we have application-wide message resources, and our actions are not arranged under a single package, we can create and define a global resources bundle. We add an initialization parameter to our web.xml
file, which defines two more message resource files:
<init-param> <param-name>struts.custom.i18n.resources</param-name> <param-value> ch03-global-messages, com/packt/s2wad/ch03/misc/misc-messages </param-value> </init-param>
These resource files would also be used to resolve message keys. Multiple files, separated by commas, can be defined, and these files may reside anywhere in the class hierarchy.
Messages may also be parameterized via the getText(String, List)
and getText(String, String[])
methods. As with getText(String),
there are also signatures that take a default message (see the API documentation for details).
Parameterized messages take positional parameters. For example, we'll put the following message that takes two parameters into a package.properties
file in the same package as the TextExamplesAction
class:
params.msg1=You passed {0} and {1}.
The {0}
and {1}
placeholders are filled with the values passed in the parameter list. Calling from Java looks exactly like we'd expect.
public String getParams1() { return getText("params.msg1", new ArrayList() {{ add("Foooo!"); add("Baaar!"); }}); }
Funky Java Alert: The code following the ArrayList
instantiation is normal Java code, although it doesn't look like it. Code like this drives people crazy, and there are situations in which it's wholly inappropriate. However, it is a useful trick to know, and it's not really much of a trick. It's left as an exercise for the reader (including determining when, and why, it might not be appropriate!).
We can also parameterize the text tag in several ways using the<s:param>
tag. The first way is to just put the parameter values in the tag body:
<s:text name="params.msg1"> <s:param>Fooooo!</s:param> <s:param>Baaaar!</s:param> </s:text>
We can also use the param
tag's value
attribute and pass in a string. It's very important to note the extra quotes around the value:
<s:text name="params.msg1"> <s:param value="'Foooo!'"/> <!-- See the extra quotes? --> <s:param value="'Baaar!'"/> <!-- Don't forget them! --> </s:text>
The value
attribute is an object, not an immediate value. In other words, the contents of the value
attribute are evaluated. If we left the quotes off, we'd see null in our output and a warning in our log file (if devMode is turned on).
This gives us a handy way to include action properties in our messages. If we add action properties named foo
and bar
to our TextExamplesAction
class, initialized to "Foooo!" and "Baaar!" respectively, we could write the following:
<s:text name="params.msg1"> <s:param value="foo"/> <s:param value="bar"/> </s:text>
Given the previous code fragments and assumptions, visiting /i18n/text-examples
will produce the same output as the text tags using the immediate values shown earlier.
Our final example of parameterized messages is pretty cool (as cool as parameterized I18N message can be) In addition to providing message parameters using a list (or array) as shown above, we can access action properties directly in our messages.
Instead of using the positional {n}
syntax, we can access action properties using the ${...}
construct, the same syntax as the param
tags. For example:
params.from.action=From action: ${foo}, ${bar}... wow!
The text tag to access this message is as we'd expect:
<s:text name="params.from.action"/>
Our action's foo
and bar
properties will be inserted into the message with no additional work on our part, saving us some JSP code. The expressions allowed in the property file are the same as those allowed in the JSP. This means that we could call action methods, including those that take arguments. This ability may not be useful all the time, but it can often save some irritating Java and/or JSP code on occasion.
LocaleProvider
allows TextProvider
to get locale information and look up resources based on Java property file names. LocaleProvider
provides one method, getLocale()
. The default locale value comes from the browser via the I18N interceptor. By overriding this method we could, for example, return a user's preferred locale from a database.
Discussing the complete functionality of Validateable
and ValidationAware
at this point is problematic. Much of the functionality depends on several Struts 2 custom tags. We'll defer those details to Chapter 6. Here we'll learn only the minimum necessary.
Validateable
contains a single method, void validate(). ActionSupport
contains a default implementation that allows for both XML-based and/or annotation-based configurations.
We can implement our own validate
method in our actions and manually perform validation . We can include default framework validation by calling super.validate().
Combining XML- or annotation-driven validation with manual validation (including "business-logic level" validation), along with the ability to define our own custom validators (covered later), covers all of our validation needs.
ValidationAware
provides a group of methods used to collect error messages related to specific form fields or the action in general. It is also used to collect general informational messages and determine if any errors or messages are present.
These two interfaces work together with the default Struts 2 interceptor stack (specifically the "validation" and "workflow" interceptors) to validate forms and direct application flow based on validation results (specifically the presence of error messages). In a nutshell, if the validation succeeds, the appropriate action method is executed normally. If the validation fails, we're returned to the "input" result.
We actually know enough at this point to prototype one of our user stories. We'll implement this story by hand as a useful exercise, and to help build appreciation for the framework (and reduce your appreciation for your author—however, I can take it!)
One of our user stories, as you can recall, was the following:
Users can enter recipes to share
The followers of more "traditional" requirement methodologies will be nothing less than horrified by this minimal description of a functional requirement. A traditional version of this story could run across a dozen pages, capturing (or attempting to capture) every nuance of the concept of "recipe entry" including the user interface design, complex validation rules, every possible form field, the underlying data model, and so on.
Once this (portion of the) specification is printed, and the toner has cooled, it will invariably be wrong. Something will have been forgotten. An edge case will have been overlooked. Half a dozen questions about the user interface will surface, all without having written any code or having used even the most minimal of recipe entry systems.
By minimizing functionality, we can very quickly create a low-fidelity prototype, allowing us to vet the basics of our system. While it's not a finished product, it's a way of determining what the finished product will be. Using our prototype can help determine user interface design decisions, spot overly-complex application flow, highlight missing form fields, and so on.
And really, that single sentence does capture the essence of what we need to do, if not all the specifics.
Since we're still learning Struts 2 and prototyping our application, we can afford to make some low-fidelity (lo-fi) assumptions. We'll assume that a "recipe" has an "id", a "name", a "description", a list of "ingredients", and some "directions".
For now, each of these will be a string (except for an Integer
id). Low-fidelity prototypes, especially early in the development cycle, don't need to reflect the final product with regards to looks, functionality, data models, and so on. Our Recipe
class is simply a POJO with appropriately named properties. The only note of interest is that the Recipe
class should have a toString()
method, giving us a human-readable version of the object.
public String toString() { return String.format("Recipe (%s) [id=%d, name=%s]", super.toString(), id, name); }
Including the default toString()
in our toString()
methods lets us tell at a glance if two string representations are the same object. This isn't always appropriate, particularly if we'd rather use toString()
output on a webpage. String.format()
was a long overdue addition!
The last story refinement is that we require the name and the ingredients, but not the description and the directions. The form will have text inputs for the name and description, and text areas for the ingredients and description (remember—this is a lo-fi prototype).
We'll now flesh out our "new recipe" JSP stub with some input tags. We'll be implementing this form by hand. If you're already familiar with the Struts 2 form tags, bear with me. Starting off on our own makes us much more appreciative of the framework.
Our recipe action will have a recipe
property, which is the Recipe
POJO described in the previous section. We'll put our form headers and input elements into a table. Here's a representative element, the recipe name (or at least what we have so far):
<tr> <td>Name</td> <td><input name="recipe.name" type="text" size="20"/></td> </tr>
The most interesting thing (so far) is the name
attribute. Struts 2 will do the right thing (in general) when encountering request parameters. It will attempt to convert them to the correct type and set the corresponding action property.
In this case, we have a nested object. The recipe
property is of the type Recipe
. The name
property of the recipe is just a string. When we see a form element name, such as the recipe.name
, what happens (more or less) is roughly equivalent to the following:
getCurrentAction().getRecipe().setName(value from form);
On top of that, Struts 2 will take care of instantiating a Recipe
object if one doesn't exist. We can verify this functionality by creating a stub action to process form submissions. We'll call it NewRecipe
(extending ActionSupport)
. However, for now, we'll just print the recipe to our standard output (hopefully), containing values from the form. Our stub action looks like this (getters and setters elided):
package com.packt.s2wad.ch03.actions.recipes; public class NewRecipe extends ActionSupport { private Recipe recipe; public String execute() throws Exception { System.out.println(recipe); return SUCCESS; } }
This is just a sanity check to show that the recipe is, indeed, being instantiated and populated. However, at this time there's no real functionality, no results, and so on.
This is another principle of agile development. Do one thing at a time, verify that it works, and then continue. Here, we're doing something similar to TDD (Test Driven Development). However, we're the testers. We'll cover TDD later.
The final piece of our current puzzle is the form tag in the JSP itself. We'll use a plain HTML form tag. As we're staying in our recipe package, and we're using Convention, we can use a relative action path. The form tag will look like this:
<form action="new-recipe" method="post">
Visiting the page at /recipes/new-recipe-input
(following the naming conventions of the Convention plug-in) should show us the form. We fill the form using "Java Soup" for the recipe name (along with other details). Now, when we submit the form and look at the console, we should see something like this:
Recipe (com.packt.s2wad.ch03.models.Recipe@921fc7) [id=null, name=Java Soup, etc...])
As unassuming as this is, it's actually pretty cool. Struts 2 instantiated a Recipe
object and filled it with values from our form without any intervention on our part. We don't need to grab values from the request parameters, or use ActionForm
. It was just a model and an action.
Now that we know we have a recipe object that can be validated, we'll do that. As our action extends ActionSupport
(which implements Validateable)
, we can override the validate()
method, which unsurprisingly performs validation. Inside the validate()
method, we use ActionSupport's
implementation of ValidationAware
, which includes methods for adding form field errors, keyed by the input element's name.
Our action's validate()
method implements our refined user story—name and ingredients are required, but not the description and the directions. We'll use Apache Commons Lang's StringUtils.isBlank()
method. There's no point in creating yet another implementation of a null or whitespace check. (Some folks disagree, and their concerns that it's another dependency and adds a large API for just one method are understandable.)
Each blank field gets an error message, keyed to the name of the input element, and added to a collection of field errors using the ValidationAware's addFieldError(String fieldName, String message)
method.
Our action's validation method is simple:
public void validate() { if (StringUtils.isBlank(recipe.getName())) { addFieldError("recipe.name", getText("recipe.name.required")); } if (StringUtils.isBlank(recipe.getIngredients())) { addFieldError("recipe.ingredients", getText("recipe.ingredients.required")); } }
The validation process depends on interceptors. For now, it's enough to know that if there are any field errors, we'll be returned to our "input" result. We'll add results to our action, an "input" result for when the validation fails, and a "success" result for when it succeeds. Our "input" result will be the same JSP reached through the /recipes/new-recipe-input
request, whereas our "success" result is a simple thank you page. We'll put the thank you page in /WEB-INF/content/recipes/ new-recipe-success.jsp
.
If we navigate back to our form and try to submit an empty form, we'll find ourselves back on the input form. However, our URL will change from /recipes/new-recipe-input
to /recipes/new-recipe
, which is what we'd expect. Adding the required fields and resubmitting shows us our thank-you page. The validation portion of the story is working, but we don't see our validation errors displayed.
ActionSupport
provides an implementation of ValidationAware's getFieldErrors()
, which returns a map of field errors keyed by field names. Each map entry is an array. We'll take a sneak peek at another Struts 2 tag,<s:if>
and use it to determine whether an error message for a specific field exists or not.
We'll add a test to each field to see if an error message exists for that field. If the message exists, we'll display it in a span. For the name
property, we end up with this:
<s:if test="getFieldErrors().get('recipe.name') != null"> <div class="error"> <s:property value="getFieldErrors().get('recipe.name')[0]"/> </div> </s:if> <input name="recipe.name" type="text" size="20" value="<s:property value="recipe.name"/>" />
For now, we can assume there's only one error message for each field, as that's all our validate()
method does. For multiple errors, we'd have to iterate over all messages. Also, we're using the<s:property>
tag to reload the field's value. Hence, our form contents are restored when the validation fails.
A simple user story turned into a lot of work! You'll either be delighted, or irritated, to learn that we didn't have to do a large portion of what we just did, thanks to Struts 2's form tags. In fact, most of our JSPs will disappear. We'll cover these tags later.
We've seen the interfaces supported by the ActionSupport
class. Combining interfaces (implemented by an action) with interceptors (either the framework's or your own) is how Struts 2 implements the bulk of its functionality.
We can implement additional interfaces to get additional capabilities. We'll cover quite a few useful interfaces here, from both XWork and Struts 2, after a brief detour to explore... ways to explore.
As a side note, reinforcing the idea of exploration and ad hoc tool creation, let's think for a moment about how to find information about the environment we're working in. I knew I wanted a list of interfaces from both XWork and Struts 2. However, if we look at the Javadocs for Struts 2 (which, by the way includes the XWork Javadocs), there's no quick way to get a list of all the interfaces.
Standard Javadocs indicate interfaces with italics in the classes frame (lower left by default). That's not a particularly efficient system discovery method, although it works. We could define our own stylesheet that highlights the interfaces, but that's a fair amount of work. We'll see a few ways (out of many available) to extract this kind of information.
If we're developing in an IDE, there's most likely some form of source or library browser available. For example, Eclipse has a Java Code Browser perspective. Selecting a package shows us a list of the defined types, an icon indicating the interface, class, and so on. We can also filter our view to show only interfaces.
We're currently interested in the interfaces provided by XWork. Browsing the Action
interface shows us something similar to the following:
This is helpful. However, if we're trying to get an overview of the system, identify relationships, create printed documentation, and so on, it's not as helpful as it could be.
One quick-and-dirty way to get information like this from a source tree (if we have access to the source code, we can also examine the contents of a JAR file) is by using the grep
command, available on all Unix-like systems (and Windows via Cygwin).
If we're running Windows and not using Cygwin, we're selling ourselves short. Cygwin provides much of a Unix-like environment, including the Bash shell and all of the incredibly useful file and text processing tools such as find, grep, sort, sed
, and so on. The importance of these tools cannot be over-emphasized, but I'll try. They're really, really important! Really! Honestly, they should be in the arsenal of every developer.
By chaining a few of these simple utilities together, we can create a list of interfaces defined in the source code. With some minor formatting, the output is a list of interfaces we can then explore via Javadocs or the source. The following is one way to get a list of all files in a source tree that declare an interface. Continuing our look at XWork, running the following command line will produce the terse, but useful output here:
$ find . -name "*.java" | xargs grep -l "public interface"
./com/opensymphony/xwork2/Action.java
./com/opensymphony/xwork2/ActionEventListener.java
./com/opensymphony/xwork2/ActionInvocation.java
./com/opensymphony/xwork2/ActionProxy.java
./com/opensymphony/xwork2/ActionProxyFactory.java
./com/opensymphony/xwork2/config/Configuration.java
./com/opensymphony/xwork2/config/ConfigurationProvider.java
Not the prettiest output, but it quickly provides a high-level view of the interfaces defined in the XWork source code without any actual effort. The argument to the grep
command is a regular expression. Never underestimate the power of regular expressions. A mastery of regular expressions is an awesome timesaver in so many circumstances, from finding a file to doing incredibly complicated search-and-replace operations.
Repeating the same command in the Struts 2 source tree would generate a list of Struts 2 interfaces. Never underestimate the usefulness of the find/grep
combination as a "first line of attack" tool set, particularly when we know regular expressions well.
The last method involves looking at Java class files, in this case, extracted from the XWork and Struts 2 core library files. From there, the ASM bytecode manipulation library is used to extract information about each interface that is defined in the class files.
Bytecode manipulation sounds scary, but it's easier (and more useful!) than you might think. There's a good chance that you're already using some libraries (such as Hibernate or Spring) that use it for some of their functionalities.
The following image was created with a Java byte code exploration tool with a Graphviz back end. The ability to query our code and libraries can be a valuable aid for understanding by suggesting areas of a framework or library to explore, and more.
We see a few interfaces we've already touched upon (Action, ValidationAware, Validateable, TextProvider, LocaleProvider)
, and several we haven't (but will).
Using the tools we already have, or the tools that we create on an as needed basis, we can facilitate development and learning efforts.
We'll take a brief look at a few more useful interfaces provided by XWork and Struts. We can use our own exploring methods (or just scan the Javadocs) to discover more. Just like it's difficult to over-emphasize how important command line mastery is, it's tough to oversell the value of exploration, writing example and test code, and so on.
The Preparable
interface works in concert with the prepare
interceptor. It defines a single method, prepare()
, which is executed before any action method. For example, this can be used to set up a data structure used in a JSP page, perform database access, and so on. Preparable
is most useful when an action has multiple methods, each needing identical initialization. Rather than calling a preparation method each time, we implement Preparable
, and the framework calls it for us.
Actions with only a single method might not benefit from Preparable
. The benefit of Preparable
is that it can remove initialization and setup code from the mainline code. The disadvantage is that it requires the developer to know that the preparable methods are being called by the framework during the request handling process.
Actions containing multiple methods can implement method-specific Preparable
methods. For example, if we have an action method doSomething()
, we'd create prepareDoSomething()
. The prepare
interceptor would call this method after the prepare()
method, if it exists.
As we've seen, the request parameters will be set as action properties by the framework. Some web applications will also need access to application-scoped, session-scoped, or request-scoped attributes. A trio of interfaces cover this requirement, another adds accessing request parameters in case we need to access them directly.
These four interfaces, each containing a single method, work in concert with the servletConfig
interceptor. They allow actions to access scoped data, without tying our actions to the servlet specification. This has a compelling consequence. We can use these actions out of our application server environment, including in standalone applications, or unit and functional tests. (We'll explore testing topics later in the book.)
void setApplication(Map application); // ApplicationAware void setSession(Map session); // SessionAware void setRequest(Map request); // RequestAware void setParameters(Map parameters); // ParameterAware
Note that we can access the same objects via ActionContext
methods. For example, we can access the session attributes with:
Map session = ActionContext.getContext().getSession();
This is, however, less testable. Therefore, implementing SessionAware
is preferable.
While it's not recommended, there are (hopefully rare) times when we absolutely need access to Servlet specification objects, rather than just their attribute or parameter maps. Implementing these interfaces will provide these objects, but tie our action to the Servlet specification.
// ServletContextAware void setServletContext(ServletContext context); // ServletRequestAware void setServletRequest(HttpServletRequest request); // ServletResonseAware void setServletResponse(HttpServletResponse response);
Similar to the attribute and parameter maps, we can also access these objects using a utility class. For example, we could access the HTTP request with:
HttpServletRequest req = ServletActionContext.getRequest();
As indicated before, this is not the preferred method, as it's more flexible to implement the interface.
Along the same line as the other *Aware
interfaces is the CookiesAware
interface, which works with the cookie
interceptor to provide our action with a map of Cookies
. This also ties our action to the Servlet specification, although perhaps with less consequence.
void setCookiesMap(Map cookies); // CookiesAware
The NoParameters
interface (interestingly defined by both XWork and Struts 2, although the Struts 2 version does not appear to add any additional functionality) effectively turns off the normal request parameter framework processing. In other words, implementing NoParameters
means that our action won't have its properties set from request parameters. This is only a marker interface and defines no methods.
The ParameterNameAware
interface allows an action to declare whether or not it will accept a parameter of a given name via the acceptableParameterName()
method. The method should return true
if the action will accept a parameter. Otherwise it should return false
.
boolean acceptableParameterName(String parameterName);
This interface also works in conjunction with the params
method. There are various ways to use this method. We could create a white list of parameters to accept, or a blacklist to ignore. We'll discover another way to get the same functionality using a parameter filter interceptor, thereby avoiding Java code. However, it's nice to know that the functionality is there.
This chapter delves into ActionSupport
. It explains how ActionSupport
, along with a few interfaces and a few Struts 2 tags, allows us to implement a fair amount of functionality. The chapter covers a bit about the I18N and validation support provided by ActionSupport
.
The chapter also takes a quick took at a few other interfaces that provide access to scoped attributes (and if needed, direct access to request parameters). We can also access Servlet specification objects, such as the request and response, if we really need to.
All of these interfaces work in concert with interceptors, which we'll discuss in detail later. Even without in-depth knowledge of interceptors, we are already using them. And we could see that they're something worth paying attention to—much of Struts 2's functionality depends on them.
Next on our plate is more about Struts 2 results, including a look at the framework's default result types. We'll also look further at result configuration, type conversion, and how to create our own custom result types.
We'll also add a bit more functionality to our recipe application, sneak a peak at some additional Struts 2 tags (that will make you wonder why I made you type all that JSP in this chapter!), and get our first look at how to fake some data—smoke and mirrors, but handy!
References
A reader can refer to the following:
Struts 2 and XWork Javadocs (reflects the latest version—may be ahead of releases!):
http://struts.apache.org/2.x/struts2-core/apidocs/index.html
Cygwin (for those of us unlucky enough not to use a Unix-like system):
Linux command line reference (not all applicable, but still useful):
ASM bytecode manipulation and analysis framework:
18.191.189.186