Ajax Dynamic Content with Struts2, JQuery and JSON plugin

This guide explains how to create a simple web application that dynamically populates a page through AJAX, using both Struts2 and the JSON features of JQuery.
First of all it’s required the Json Plugin (available at http://jsonplugin.googlecode.com) that should be placed in the /WEB-INF/lib/ directory (where obviously are placed all the Struts2 jar as explained in other tutorials of this site).

The plugin adds (through its struts-plugin.xml) a new result type defined this way:

<package name="json-default" extends="struts-default">
 <result-types>
  <result-type name="json" class="com.googlecode.jsonplugin.JSONResult"/>
 </result-types>
 ...
</package>

Since it’s defined in the json-default package, in order to use that result inside custom action mappings, there are two choices:

  • the packages containing actions with json result-type have to extend the package json-default and not, as usual, the struts-default package
  • in the packages where json result-type is used, it’s to possible to add the previous <result-types>...</result-types> lines that simply refers to a class contained in the json plugin .jar added to the application.

It’s now possible to define action mappings using json as result type, like the following one:

<package name="testPackage" extends="json-default" namespace="/test">
 <action name="giveMeJsonData" class="testAction" method="giveMe">
  <result type="json">
   <param name="root">jsonData</param>
  </result>
 </action>
...
</package>

The above definition states that the url /test/giveMeJsonData.action will cause the execution of the method public String giveMe() defined inside the class testAction (in this case it’s a Spring managed bean, but it can be even a qualified name of a Struts2 action class, obviously extending ActionSupport class).

The result of that action (with a SUCCESS result code) is the json data structure stored in the jsonData property of the action class, and so available through its getter getJsonData().

An example of the behavior for giveMe() method:

public String giveMe() {
 jsonData = new LinkedHashMap<String, Object>();
 jsonData.put("shoppingCartId", getCartId());
 jsonData.put("datetime", new Date());
 Set<Map<String, Object>> items = new HashSet<Map<String, Object>>();
 for (Item item : businessMethod.findItemsForCart(getCartId())) {
  HashMap<String, Object> itemMap = new HashMap<String, Object>();
  itemMap.put("id", item.getId());
  itemMap.put("quantity", item.getQuantity());
  itemMap.put("price", item.getPrice);
  items.add(itemMap);
 }
 jsonData.put("items", items);
 return SUCCESS;
}

The final step is to use JQuery to call (on a specific event) through AJAX the URL where the action has been defined, and obviously to use the returned data to dynamically populate the page HTML.

function testingJsonAndAjax(cartId) {
 $.getJSON(
  /test/giveMeJsonData.action ,
  {
   cartId: cartId
  },
  function(json) {
   $('#cartId').html(json.shoppingCartId);
   $('#cartCreation').html(json.datetime);
   itemsHtml = "<table>";
   for (i in json.items) {
    itemsHtml += “<tr>”;
    itemsHtml += “<td>” + json.items[i].id + “</td>”;
    itemsHtml += “<td>” + json.items[i].quantity + “</td>”;
    itemsHtml += “<td>” + json.items[i].price + “</td>”;
    itemsHtml += “</tr>”;
   }
   itemsHtml += “</table>”;
   $('#cartItems').html(itemsHtml);
  }
 );
 return false;
}

A sample HTML would look like this

Cart 32233 <a href=”#” onclick="return testingJsonAndAjax(32233)>Refresh</a> <br />
Cart 82382 <a href=”#” onclick="return testingJsonAndAjax(82382)>Refresh</a> <br />

<div id=”cartId”>JQuery will replace this text with the Cart Id returned by the json action</div>
<div id=”cartCreation”>JQuery will replace this text with the Cart creation date returned by the json action</div>
<div id=”cartItems”>Jquery wil replace this text with a HTML table containg all the items of the selected cart</div>

The id will be used by the JQuery selector to determine in which of them the data returned by the json action will be written in.

I hope this tutorial has been useful for a simple introduction to AJAX and JSON using JQuery and Struts2.

EDIT 1: on the Struts User Mailing List, Wes Wannemacher suggested that it would be better to directly put the item object inside the root object returned through JSON.

This is absolutely right and would lead to cleaner code. But I didn’t used that technique for a security reason, i.e. if the Item object is a JPA entity, it may contain some properties that is better not to show to the end users. In the case of a User entity, it would be no good to return in the json data its hashed password.

So I created that ugly workaround, defining some HashMaps and putting there just the specific properties I wish to return in the Json result (and maybe this will save too some HTTP traffic 🙂 )

EDIT 2: on the Struts User Mailing List, Nils-Helge Garli Hegvik suggested that it’s even possible to use the “includeProperties” or “excludeProperties” parameters (as described here) in the result configuration to simply return some objects and the JSON plugin will do the trick of filtering just the specific properties to show.

in the packages where “json” result-type is used, it’s to possibile to add the previous &lt;result-types&gt;…&lt;/result-types&gt; lines that simply refers to a class contained in the .jar added to the application (contained in the json-plugin jar).

REST web application with Struts2.1 Rest and Convention plugins

The sample application developed in this tutorial will handle a simple message system, where it’s possible to read, add and remove messages.
So, the basic bean will be defined in the Message class:

package com.zulutown.struts2.rest;

public class Message {
	private String id;
	private String text;
	private String author;

	public Message() {
		super();
	}

	public Message(String id, String text, String author) {
		super();
		this.id = id;
		this.text = text;
		this.author = author;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getText() {
		return text;
	}

	public void setText(String text) {
		this.text = text;
	}

	public String getAuthor() {
		return author;
	}
	public void setAuthor(String author) {
		this.author = author;
	}

	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		return result;
	}

	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Message other = (Message) obj;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		return true;
	}
}

The message will be identified by the id property and will contain its text and its author in the other two class properties.

The next step is to setup a basic business service that will handle the basic operations on the messages: find, save, remove and so on. In a real world application this service would be a singleton that interacts with a database (through JDBC or JPA) but in this little demo the data model is just a very basic (and ugly) Map kept in a static property of the Message class.

package com.zulutown.struts2.rest;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MessageService {

	public static Map messages = new HashMap();
	private static int nextMessageId = 4;

	static {
		Message message1 = new Message("1", "hello", "john");
		Message message2 = new Message("2", "world", "ted");
		Message message3 = new Message("3", "rest", "sam");
		messages.put("1", message1);
		messages.put("2", message2);
		messages.put("3", message3);
	}

	public static List findAll() {
		return new ArrayList(messages.values());
	}

	public static Message find(String id) {
		return messages.get(id);
	}

	public static void save(Message message) {
		if (message.getId() == null) {
			String id = String.valueOf(nextMessageId);
			message.setId(id);
			nextMessageId++;
		}
		messages.put(message.getId(), message);
	}

	public static void remove(String id) {
		messages.remove(id);
	}
}

In messages it is stored the data and the static initializer of the MessageService class will populate the data model with three messages.
Then, some utility methods are provided: findAll() to retrieve all the messages, find(String id) to retrieve a message with a specific id, save(Message message) to add (or update) a message in the data model and, finally, a remove method to delete a specific message.

Until now, this tutorial is not related at all with Struts 2.1, but now it is required to add a “web layer” to the application, and the REST capabilities provided by Struts2 will be used.

The first task is to add to the Web application the .jar libraries of Struts2. Those files must be placed into /WEB-INF/lib (in WebContent) that is the standard location where a Java Web Application expects to find its dependencies.

Download from the Struts website the archives with the libraries for Struts 2.1.x (currently the GA release is 2.1.6), unpack it, and copy & paste the following jars in /WEB-INF/lib/ of your project.

  • commons-beanutils-1.7.0.jar
  • commons-collections-3.2.jar
  • commons-fileupload-1.2.1.jar
  • commons-io-1.3.2.jar
  • commons-lang-2.3.jar
  • commons-logging-1.0.4.jar
  • ezmorph-1.0.3.jar
  • freemarker-2.3.13.jar
  • json-lib-2.1.jar
  • ognl-2.6.11.jar
  • struts2-convention-plugin-2.1.6.jar
  • struts2-core-2.1.6.jar
  • struts2-rest-plugin-2.1.6.jar
  • xpp3_min-1.1.3.4.O.jar
  • xstream-1.2.2.jar
  • xwork-2.1.2.jar

Two Struts2 plugins are used:

  • Rest Plugin
  • Convention Plugin

First of all create at the root of the src directory the struts.xml file:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.1//EN"
    "http://struts.apache.org/dtds/struts-2.1.dtd">

<struts>
    <constant name="struts.convention.package.locators" value="rest"/>  
    <constant name="struts.convention.action.suffix" value="Controller"/>
    <constant name="struts.convention.action.mapAllMatches" value="true"/>
    <constant name="struts.convention.default.parent.package" value="rest-default"/>
</struts>

Convention plugin makes possible to map classes and methods on automatically generated URLs. It is not required the manual “wiring” usually done in the struts.xml configuration file that, in this case, contains just a few lines of configuration. The property struts.convention.package.locators defines the package name where the Convention plugin will look for Struts2 Actions and Controllers, then with struts.convention.action.suffix it is specified that just the classes with the Controller suffix will be automatically mapped. The REST controller that’s going to be defined in the next step, will have the name MessagesController and will be contained in the package com.zulutown.struts2.rest, then it will match both the configurations described above. Given this configuration, the list of the messages in XML format will be accessible through an HTTP GET method on http://localhost:8080/Struts2-Rest/messages.xml, and to obtain the JSON messages list, it’s enough to call the HTTP GET method on http://localhost:8080/Struts2-Rest/messages.json. Simple, isn’t? Now, the simple code of the Controller class that will handle the REST requests:

package com.zulutown.struts2.rest;

import java.util.Collection;
import org.apache.struts2.rest.DefaultHttpHeaders;
import org.apache.struts2.rest.HttpHeaders;
import com.opensymphony.xwork2.ModelDriven;

public class MessagesController implements ModelDriven<Object> {

	private static final long serialVersionUID = 89268916175477696L;
	private Message model = new Message();
	private String id;
	private Collection<Message> list;

	public HttpHeaders create() {
		MessageService.save(model);
		return new DefaultHttpHeaders("create");
	}

	public HttpHeaders destroy() {
		return new DefaultHttpHeaders("destroy");
	}

	public HttpHeaders show() {
		return new DefaultHttpHeaders("show").disableCaching();
	}

	public HttpHeaders update() {
		MessageService.save(model);
		return new DefaultHttpHeaders("update");
	}

	public HttpHeaders index() {
		list = MessageService.findAll();
		return new DefaultHttpHeaders("index").disableCaching();;
	}

	public Object getModel() {
		return (list != null ? list : model);
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		if (id != null) {
			this.model = MessageService.find(id);
		}
		this.id = id;
	}
}

In the Struts2 REST plugin, the method name (in the the Controller class) identifies which kind of operation should be executed on specific HTTP requests.

HTTP GET method on http://localhost:8080/Struts2-Rest/messages.xml calls index() and provides the list of all the messages (in the model property of the controller).

The result is:

<list>
  <com.zulutown.struts2.rest.Message>
    <id>3</id>
    <text>rest</text>
    <author>sam</author>
  </com.zulutown.struts2.rest.Message>
  <com.zulutown.struts2.rest.Message>
    <id>2</id>
    <text>world</text>
    <author>ted</author>
  </com.zulutown.struts2.rest.Message>
  <com.zulutown.struts2.rest.Message>
    <id>1</id>
    <text>hello</text>
    <author>john</author>
  </com.zulutown.struts2.rest.Message>
</list>

HTTP GET method on http://localhost:8080/Struts2-Rest/messages/2.xml calls setId("2") (that loads in the model property the Message identified by the provided id) then show().

The result is:

<com.zulutown.struts2.rest.Message>
  <id>2</id>
  <text>world</text>
  <author>ted</author>
</com.zulutown.struts2.rest.Message>

The previous two methods are easy to call with the browser but, to test the other HTTP methods, it’s better to use the Firefox plugin Poster.

HTTP POST method on http://localhost:8080/Struts2-Rest/messages.xml injects into model a new message, then calls create() that persists it.

In the poster dialog box, the URL must be http://localhost:8080/Struts2-Rest/messages.xml with POST action. In Content to Send it’s required to insert the XML entity for the new message (without specifying its id, because it will be automatically generated by MessageService).

  <com.zulutown.struts2.rest.Message>
    <text>new text</text>
    <author>new author</author>
  </com.zulutown.struts2.rest.Message>
Rest - HTTP Post

Rest - HTTP Post

HTTP PUT method on http://localhost:8080/Struts2-Rest/messages/2.xml calls setId("2") (that causes the loading of the existing message with id=2 in the model property) then, depending on which fields are specified in the XML, those message properties are modified and finally a call to update() saves the updated message.

In the poster dialog box, the URL must be http://localhost:8080/Struts2-Rest/messages/2.xml with PUT action. In Content to Send it’s required to insert the XML entity for the fields to edit (the unspecified fields will keep the previous values).

  <com.zulutown.struts2.rest.Message>
    <text>updated text</text>
  </com.zulutown.struts2.rest.Message>
Rest - HTTP Put

Rest - HTTP Put

After the POST and PUT calls, the data structure will look like this:

<list>
  <com.zulutown.struts2.rest.Message>
    <id>3</id>
    <text>rest</text>
    <author>sam</author>
  </com.zulutown.struts2.rest.Message>
  <com.zulutown.struts2.rest.Message>
    <id>2</id>
    <text>updated text</text>
    <author>ted</author>
  </com.zulutown.struts2.rest.Message>
  <com.zulutown.struts2.rest.Message>
    <id>1</id>
    <text>hello</text>
    <author>john</author>
  </com.zulutown.struts2.rest.Message>
  <com.zulutown.struts2.rest.Message>
    <id>4</id>
    <text>new text</text>
    <author>new author</author>
  </com.zulutown.struts2.rest.Message>
</list>

In this tutorial all the URLs refer to xml content type, but it’s possible to use json just changing URLs from messages.xml to messages.json or messages/2.xml to messages/2.json.

I hope this tutorial has given a simple and easy introduction to the REST capabilities of Struts2.1. Please leave your comments and feedback.