Thursday, June 3, 2010

Cascading dropdowns with AJAX on Java using DWR

This is one subject in Java that took me a helluva lot to crack. This subject literally took me through a journey to learn javascripting/ajax frameworks like Dojo, JQuery, etc. But finally landed on DWR to solve my problem.
I have usually been a loyal JSF person. And in JSF, ajax is almost always handled by the rich set of components provided by the various frameworks like Trinidad, Tomahawk, RichFaces, etc. But I had taken to develop my web application using Struts 2, which I heard is the raging in thing these days. I had worked with Struts 1 longtime ago and when I looked at picking up Struts 2 I was expecting the usual ActionForms, etc. But was surprised to find it a bit closer to JSF than I expected. I liked everything in struts 2 expect that Ajax had to be done manually. That is still okay if you have some good resources to learn how to do it with struts.
The first ajax problem I had hit upon was to get cascading dropdowns working. Everytime I searched for ajax and struts I would get patchy references of dojo-struts combination or struts' sx tag library. I had seen the publish-subscribe mechanism being used in a few examples but no resource explaining what is happening and how. Then by accident I stumbled upon DWR and god bless them for a more extensive documentation than others.
What I liked about DWR is
  • That it makes me to avoid the url definitions in the client side.
  • The call to server code is almost the same as calling the actual java code on the server.
  • You can test that the ajax call is succeeding through a url that dwr exposes on your web application. It is usually in the form: http://localhost:8080//dwr/
  • It has some very good built-in functions to populate textfields, textareas, select components, etc. apart from nifty functions to get their values too.
So without much more ado let's dive into an tutorial of how to get a struts and dwr example running:
1. Project setup: Assuming that you are not new to maven 2, this is the best option to get started with this project. If you are new then please take the time out to get started with it. It is well worth the time. Go to your projects folder where you start new projects and issue the following command:
mvn archetype:create -DgroupId=com.struts.dwr.tutorial -DartifactId=tutorial -DarchetypeGroupId=org.apache.struts -DarchetypeArtifactId=struts2-archetype-starter -DarchetypeVersion=2.0.11.2-SNAPSHOT -DremoteRepositories=http://people.apache.org/repo/m2-snapshot-repository

Note at the time of writing the only version of struts2-archetype-starter available was 2.0.11.2-SNAPSHOT. This command will create a ready made struts-dwr project folder which you can immediately take for a ride.

Also note that this command creates a pom.xml file which loads dwr of version 1.1-beta-3. But we want to work with version 2.0.3. So, open the tutorial/pom.xml file and change the dwr dependency to look like this:
<dependency>
<groupId>org.directwebremoting</groupId>
<artifactId>dwr</artifactId>
<version>2.0.3</version>
</dependency>


2. Test the new Project: Test the newly created project by issuing the following command in the project folder i.e. the tutorial folder:

mvn jetty:run


This will start a jetty server. And you should be able to see the running web application at the following url: http://localhost:8080/tutorial
Tip: Even if you are struggling to find out the exact url at which you might find your web application, with jetty you can just go to the url http://localhost:8080 and it will tell you the url for all the web applications that are deployed in it. Sweet, right?

3. Load the project into eclipse: Now is the time to do some development. And without eclipse (assuming that that is the ide you are using, of course :) ). But you will need to prepare the project to be accepted by eclipse. To do that you will need to issue the command in the project folder:
mvn eclipse:eclipse
This will create the .classpath and .project files in the project root folder. With that in place you can import this project into eclipse. I won't go into how to do that.
Tip: Usually, this command is unable, for now, to make the eclipse project file so that it is recognized as a web project by eclipse. But that can be done using tips from this url: http://greatwebguy.com/programming/eclipse/converting-a-java-project-to-a-dynamic-web-project-in-eclipse/ or you can open the .project file of a dynamic web project you might've previously created and make the current .project file to look like it. Why is it important for the project to be recognized as a web project by eclipse? Because, otherwise, you will not get the design screen goodies for jsp files or faces-config.xml files, for instance.

4. Coding: Now let us dive right into the task of creating the Action and Model classes and the jsp file. We will have only three dropdowns on our page: country, state and city. Therefore we need three model classes Country, State and City. We will need one action class and one jsp page. Following is the code for them:
Country.java:
package com.struts.dwr.tutorial.model;

public class Country {

private int id;

private String name;

public Country(int id, String name) {
this.id = id;
this.name = name;
}

public int getId() {
return id;
}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}

State.java:
package com.struts.dwr.tutorial.model;

public class State {

private int id;

private String name;

private int countryId;

public State(int id, String name, int countryId) {
this.id = id;
this.name = name;
this.countryId = countryId;
}

public int getId() {
return id;
}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getCountryId() {
return countryId;
}

public void setCountryId(int countryId) {
this.countryId = countryId;
}

}


City.java:
package com.struts.dwr.tutorial.model;

public class City {

private int id;

private String name;

private int stateId;

public City(int id, String name, int stateId) {
this.id = id;
this.name = name;
this.stateId = stateId;
}

public int getId() {
return id;
}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getStateId() {
return stateId;
}

public void setStateId(int stateId) {
this.stateId = stateId;
}

}


CascadingDDAction.java:
package com.struts.dwr.tutorial.action;

import java.util.ArrayList;
import java.util.List;

import com.opensymphony.xwork2.ActionSupport;
import com.struts.dwr.tutorial.model.City;
import com.struts.dwr.tutorial.model.Country;
import com.struts.dwr.tutorial.model.State;

public class CascadingDDAction extends ActionSupport {

List countries;

List cities;

List states;

public String input() {
countries = new ArrayList();
countries.add(new Country(1, "INDIA"));
countries.add(new Country(2, "US"));
states = new ArrayList();
states.add(new State(-1, "Other", -1));
cities = new ArrayList();
cities.add(new City(-1, "Other", -1));
return INPUT;
}

public List getStatesByCountryId(int countryId) {
List result = new ArrayList();
if (countryId == 1) {
result.add(new State(1, "Andhra Pradesh", 1));
result.add(new State(2, "Gujarat", 1));
} else if (countryId == 2) {
result.add(new State(3, "California", 2));
result.add(new State(4, "New Jersey", 2));
}
return result;
}

public List getCitiesByStateId(int stateId) {
List result = new ArrayList();
if (stateId == 1) {
result.add(new City(1, "Hyderabad", 1));
result.add(new City(2, "Secunderabad", 1));
} else if (stateId == 2) {
result.add(new City(3, "Ahmedabad", 2));
result.add(new City(4, "Vadodara", 2));
} else if (stateId == 3) {
result.add(new City(5, "Fremont", 3));
result.add(new City(6, "San Francisco", 3));
} else if (stateId == 4) {
result.add(new City(7, "Edison", 4));
result.add(new City(8, "Plainsboro", 4));
}
return result;
}

public List getCountries() {
return countries;
}

public List getStates() {
return states;
}

public List getCities() {
return cities;
}
}

Tip:Note that I have used the action class to write the methods that give out the states and cities based on countryId and stateId, respectively, to save space.


cascadingDD.jsp:
<%@taglib prefix="s" uri="/struts-tags"%>

<s:select list="countries" label="Country" listKey="id" listValue="name" />
<s:select id="stateId" list="states" label="State" listKey="id" listValue="name" />
<s:select id="cityId" list="cities" label="City" listKey="id" listValue="name" />



Add the following lines in the struts.xml:

<action name="cascadingdd" class="com.struts.dwr.tutorial.action.CascadingDDAction" method="input">
<result name="input">/jsp/cascadingDD.jsp</result>
</action>



With the above code in place you will be able to run the web-app and see the non-cascading dropdowns working in the following url: http://localhost:8080/tutorial/cascadingdd.action

5. Adding the magical DWR ajax stuff: Now is the time to add the magical ajax stuff. Do the following steps:
1. Modify the cascadingDD.jsp to look like this:
<%@taglib prefix="s" uri="/struts-tags"%>

<script type="text/javascript" src="<s:url value='/dwr/interface/CascadingDDAction.js'/>"></script>
<script type="text/javascript" src="<s:url value='/dwr/engine.js'/>"></script>
<script type="text/javascript" src="<s:url value='/dwr/util.js'/>"></script>
<script type="text/javascript">
function updateState(countryId){
CascadingDDAction.getStatesByCountryId(countryId, function(states){
dwr.util.removeAllOptions("stateId");
dwr.util.addOptions("stateId", [{"id":-1,"name":"Select one.."}], "id", "name");
dwr.util.addOptions("stateId", states, "id", "name");
});
}
function updateCity(stateId){
CascadingDDAction.getCitiesByStateId(stateId, function(cities){
dwr.util.removeAllOptions("cityId");
dwr.util.addOptions("cityId", [{"id":-1,"name":"Select one.."}], "id", "name");
dwr.util.addOptions("cityId", cities, "id", "name");
});
}
</script>

<s:select list="countries" label="Country" listKey="id" listValue="name" onchange="updateState(this.value)"/>
<s:select id="stateId" list="states" label="State" listKey="id" listValue="name" onchange="updateCity(this.value)"/>
<s:select id="cityId" list="cities" label="City" listKey="id" listValue="name" />

2. Add the following lines in the element in the dwr.xml file:

<create creator="new" javascript="CascadingDDAction">
<param name="class" value="com.struts.dwr.tutorial.action.CascadingDDAction"/>
<include method="getStatesByCountryId"/>
<include method="getCitiesByStateId"/>
</create>
<convert match="java.util.List" converter="collection"></convert>
<convert match="com.struts.dwr.tutorial.model.City" converter="bean"></convert>
<convert match="com.struts.dwr.tutorial.model.State" converter="bean"></convert>


That's it! Crank up the jetty server and check this url to see if your methods are performing as designed: http://localhost:8080/tutorial/dwr
Then go to the url http://localhost:8080/tutorial/cascadingdd.action and see if your test case is performing as we want it to.

I think that the javascript code is pretty self-explanatory even for beginners. That's all it takes to get ajax working on struts! For more details on the dwr functions please read the documentation on their site. It is good!