Simplifying XmlHttpRequest with JSON-RPC
In my day job, we decided to use a little XMLHttpRequest lovin' to populate one drop-down from another. This is my review of JSON-RPC, an open source JavaScript library and servlet for simplifying XMLHttpRequest. I considered integrate Direct Web Remoting (DWR) as well, but its java.net site was down the day I needed it. I started out with JSON-RPC 0.7, which caused some conflicts with Commons Validator client-side validation. This was fixed in the 0.8 release. JSON-RPC takes a little more setup than I care for, but it's pretty easy nonetheless:
- Download the 0.8 release from http://oss.metaparadigm.com/jsonrpc-dist/json-rpc-java-0.8.tar.gz.
- Add the JAR to your project and the webapps/jsonrpc/jsonrpc.js to your projects' "scripts" folder. Include this file in your SiteMesh decorator or Tiles layout. If you're not using SiteMesh or Tiles, it's high time you started.
- JSON-RPC currently requires that you register each class you want call
methods on. In our project, I registered a Spring bean (LookupHelper)
that's a singleton with references to Maps in the ServletContext. Then
we used JavaScript functions to call JSON-PRC and look up units for
a plant, and vice versa. I'm not going to put the LookupHelper class
here - you'll have to trust its methods return a single String
or a comma-separated list of Strings. To register this bean with JSON-RPC,
I created an HttpSessionListener and configured it in web.xml.
/**
* UserListener class used to add/remove session attributes when
* a user first logs in. Mainly for JavaScript Remote Scripting stuff.
*
* @author Matt Raible
*/
public class UserListener implements HttpSessionListener, HttpSessionAttributeListener {
private final Log log = LogFactory.getLog(UserListener.class);
public final static String BRIDGE_KEY = "JSONRPCBridge";
/**
* Initializes LookupHelper singleton with values needed for lookup
*
* @param event the HttpSessionEvent to grab session information from
*/
public void sessionCreated(HttpSessionEvent event) {
// Find the JSONRPCBridge for this session or create one
// if it doesn't exist. Note the bridge must be named BRIDGE_KEY
// in the HttpSession for the JSONRPCServlet to find it.
HttpSession session = event.getSession();
JSONRPCBridge jsonBridge = new JSONRPCBridge();
jsonBridge.setDebug(true);
session.setAttribute(BRIDGE_KEY, jsonBridge);
}
/**
* Destroys LookupHelper
*
* @param event the HttpSessionEvent to grab session information from
*/
public void sessionDestroyed(HttpSessionEvent event) {
if (event.getSession() != null) {
event.getSession().removeAttribute(BRIDGE_KEY);
}
}
public void attributeAdded(HttpSessionBindingEvent event) {
if (event.getName().equals(BRIDGE_KEY)) {
HttpSession session = event.getSession();
// register LookupHelper so we can call methods on it
ApplicationContext ctx =
WebApplicationContextUtils
.getWebApplicationContext(session.getServletContext());
// check for null so we don't have to initialize Spring in tests
if (ctx != null) {
log.debug("Registering lookupHelper for XmlHttpRequest...");
JSONRPCBridge jsonBridge =
(JSONRPCBridge) session.getAttribute(BRIDGE_KEY);
jsonBridge.registerObject("lookupHelper",
ctx.getBean("lookupHelper"));
}
}
}
public void attributeRemoved(HttpSessionBindingEvent event) {
// don't care
}
public void attributeReplaced(HttpSessionBindingEvent event) {
// same as attribute added
attributeAdded(event);
}
} - After this setup was complete, I was able to add the following JavaScript
to the bottom of my JSP. These are functions that our drop-downs call
to populate each other, and keep their options in synch.
<script type="text/javascript">
var jsonurl = "${ctx}/jsonrpc";
var jsonrpc = null;
var unitDropDown = document.getElementById("equipmentName");
function filterUnits(plantDropDown) {
var plantName = plantDropDown.options[plantDropDown.selectedIndex].value;
if (plantName == "") {
reloadUnits("");
return;
}
try {
jsonrpc = new JSONRpcClient(jsonurl);
} catch(e) {
alert(e);
}
// Call a Java method on the server
var units = jsonrpc.lookupHelper.getUnitsForPlant(plantName);
setUnits(units);
}
function reloadUnits(value) {
if (value == "") {
try {
jsonrpc = new JSONRpcClient(jsonurl);
} catch(e) {
alert(e);
}
// Call a Java method on the server
var units = jsonrpc.lookupHelper.getAllUnits();
setUnits(units);
}
}
function setUnits(units) {
var unitArray = units.split(",");
unitDropDown.options.length = 1; // keep "All" option
for (i=0; i < unitArray.length; i++) {
unitDropDown.options[unitDropDown.options.length] =
new Option(unitArray[i], unitArray[i]);
}
}
</script>
The hardest part of using JSON-RPC is setting it up. We only experienced minor issues with Commons Validator, but since the JSON-RPC 0.8 release - everything has worked great, on all browsers we need to support. The only thing I don't like about this library is that you have to register objects for each user's session. I briefly looked at DWR and it looks a little cleaner - especially b/c of its Spring integration. The next time we need XMLHttpRequest, we'll probably use DWR just to compare the two.