Two key technologies enable next-generation Web sites: Ajax and JSON. Business-line applications can benefit from these technologies to provide more intuitive and responsive user interfaces. This article describes how to add Ajax and JSON to Java™ Platform, Enterprise Edition (Java EE) Web applications by building reusable JSP TagLib controls based on Ajax.
Within the article, I show how to build a cascading
drop-down control that dynamically populates values in an HTML SELECT control based on other form field values. I also describe how to build an auto-complete control, similar to Google Suggest, that
displays a suggestion list that is updated in real time as a user types. You'll
build the controls by integrating JSON, JavaScript, CSS,
HTML, and Java EE technologies.
The primary design goals of the controls developed in this article are:
- Provide easy integration with existing Web applications. The controls should encapsulate all logic and JavaScript code to simplify the deployment process.
- Be configurable.
- Minimize data- and page-size overhead.
- Leverage CSS and HTML standards.
- Provide cross-browser support (Microsoft® Internet Explorer, Mozilla Firefox).
- Leverage common design patterns/best practices to improve code maintainability.
To meet the goals of easy integration and configurable controls, the article's examples use configurable tag attributes where possible. In addition, you define interface/contracts to provide a straightforward way to integrate custom data/value providers with the controls.
The article uses an additional control to encapsulate common JavaScript functions, and thereby minimizing data and overhead. You use JSON to minimize data exchange when making asynchronous calls.
The articles's examples use Web standards, including CSS and HTML, for cross-browser support. The JavaScript, HTML, and CSS emitted by the controls are tested against Internet Explorer 7.x and Mozilla Firefox 2.x/3.x.
The data and value providers are built based on common Object-Oriented Programming design patterns and best practices, such as an n-tier architecture, the adapter design pattern, and interface-based programming.
Technical considerations for implementing the example controls
There are a few key technical considerations for the Ajax-enabled controls you develop in this article, including the mechanism to provide values to the Ajax controls, a data-exchange format for asynchronous communication, the class design, and the data model.
Mechanism to provide responses to asynchronous calls
You have three options to expose data asynchronously to Ajax-enabled controls:
- JavaServer Pages (JSP)
- Servlets
- SOAP or RESTful Web services
This article uses Servlets due to their efficiency and minimal overhead. A JSP page is simpler to implement than a Servlet, but it isn't as clean from an implementation perspective.
Data-exchange format considerations
Data providers for Ajax-enabled controls can use XML or JSON as the data-exchange format. XML is generally more human-readable than JSON, but it has the following disadvantages:
- Larger data size compared to JSON
- Slightly more difficult to parse within JavaScript
For these reasons, this article uses JSON.
The data model for the sample application comprises two entities:
- State, which contains state abbreviations and names
- Location, which contains city, zip code, and other location data
Figure 1 shows the data model used for the sample pages in this article.
Figure 1. Data Model
The example in this article consists of the Data Abstract Layer (DAL), Data Transfer Objects (DTOs), the Business Logic Layer (BLL), the Presentation Layer, and supporting helper classes. The following figures show UML class diagrams for these classes.
The helper classes provide database and presentation layer supporting classes (see Figure 2).
Figure 2. UML class diagram -- helper classes
The Data Abstract Layer consists of a single class to provide location-related data to the business layer (see Figure 3).
Figure 3. UML class diagram -- Data Abstract Layer classes
You use two DTOs to pass data through the three tiers (see Figure
4). StateDTO holds state-related data, and LocationDTO holds location-related data, including zip
code, city name, state, latitude, and longitude.
Figure 4. UML class diagram -- Data Transfer Object classes
The Business Logic Layer consists of the value providers that provide data to the
Ajax-enabled controls (see Figure 5). Value providers for the
auto-complete control must implement the IJsonValueProvider interface. The location service receives the collection
of DTO objects from the data layer and generates the corresponding JSON data for use
in the presentation layer.
Figure 5. UML class diagram -- Business Logic Layer classes
The Servlets provide the interface to which the client-side asynchronous calls are made (see Figure 6). These Servlets interact with the value providers to provide JSON data to the Web browser.
Figure 6. UML class diagram -- Business Logic Layer, Servlet classes
You'll create the following Ajax-enabled controls:
- Cascading drop-down control -- Dynamically populates value options in
SELECTcontrols based on other form fields or business rules. - Auto-complete control -- Displays a suggestion list, similar to Google Suggest, in real time as a user types. The suggestions are dynamically displayed using asynchronous communication with a data provider servlet.
In addition to the two JSP TagLib controls, you need a third control to encapsulate all reusable JavaScript functions, such as clearing/populating values, handling keyboard/mouse events, and supporting asynchronous communication. Figure 7 illustrates these three control classes.
Figure 7. UML class diagram -- JSP TagLib control classes
Build the data provider and data layer
The LocationDataService class
is a data provider to retrieve location-related data from the database. It returns
TreeMap objects containing LocationDTO and StateDTO objects. It's highly
recommended that the data provider cache the results in memory for optimal
performance, particularly because the data is consumed through asynchronous server
calls.
You create a JSP TagLib control by extending TagSupport or TagBodySupport and
overriding the doStartTag(), doAfterBody(), or doEndBody() method to
render the control's content (HTML code, JavaScript) during page processing. Listing 1 shows an example of an overridden doStartTag
method.
Listing 1. JdbcQuery class
/* (non-Javadoc)
* @see javax.servlet.jsp.tagext.TagSupport#doStartTag()
*/
@Override
public int doStartTag() throws JspException {
JspWriter out = pageContext.getOut();
try {
// An example of rendering output within a JSP page
out.print("This is a string that will be rendered");
// A more practical example
out.print("<h1 id='heading1'>This is a Heading</h1>");
} catch (IOException e) {
e.printStackTrace();
}
|
After you create the implementation of the JSP TagLib control, you must define a TagLib Library Definition (TLD) in the /WEB-INF/tlds directory, as shown in Listing 2.
Listing 2. Sample JSP TagLib library definition file
<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN" "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd"> <taglib> <tlibversion>1.0</tlibversion> <jspversion>1.1</jspversion> <shortname>ajax</shortname> <info>Ajax control library</info> <tag> <name>sample</name> <tagclass>com.testwebsite.controls.SampleJspTag</tagclass> <bodycontent>JSP</bodycontent> <info> This is a sample control </info> <attribute> <name>id</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> </tag> </tagLib> |
You can place the control in any JSP page by adding the code from Listing 3.
Listing 3. Sample JSP page
<%@ page contentType="text/html; charset=ISO-8859-5" %>
<%@ taglib prefix="ajax" uri="/WEB-INF/tlds/ajax_controls.tld"%>
<html>
<head>
<title>This is a test page</title>
<link href="core.css" rel="stylesheet" type="text/css" />
</head>
<body>
This is a test page.
<ajax:sample/>
</body>
</html>
|
Build the Ajax page JSP TagLib control
The <ajax:page/> control
renders the standard JavaScript functions that are needed to add asynchronous
support to JSP pages. It also renders helper functions for the <ajax:autocomplete/> and <ajax:dropdown/> controls. The helper functions are covered in the
respective control sections Build the auto-complete
JSP TagLib control and Build the cascading
drop-down JSP TagLib control. Where possible, it's best to keep supporting
JavaScript functions in the <ajax:page/> control
rather than in individual controls because doing so reduces the page size.
Alternatively, you can store them in an external JS file, but that slightly
complicates deployment by reducing encapsulation within the control.
The XMLHttpRequest object, which is accessible in JavaScript,
is central to asynchronous Web communication. Unfortunately, XMLHttpRequest isn't an approved standard, and vendor support varies
slightly. For Opera, Mozilla Firefox, and Microsoft Internet Explorer 7.0 and later,
it's a matter of using the new XMLHttpRequest()
JavaScript syntax. For prior versions of Microsoft Internet Explorer, you create the
object using new ActiveXObject('Microsoft.XMLHTTP'). Listing 4 shows how to initialize the XMLHttpRequest for cross-browser
support.
Listing 4. Create the XMLHttpRequest object
var req;
function initializeXmlHttpRequest() {
if (window.ActiveXObject) {
req=new ActiveXObject('Microsoft.XMLHTTP');
}
else {
req=new XMLHttpRequest();
}
}
|
As mentioned, you render JavaScript code to a page by adding the code from Listing 5 to the tag-implementation class.
Listing 5. Render the JavaScript initialization function for the XMLHttpRequest object
/* (non-Javadoc)
* @see javax.servlet.jsp.tagext.TagSupport#doStartTag()
*/
@Override
public int doStartTag() throws JspException {
StringBuffer html = new StringBuffer();
html.append("<script type='text/javascript' language='javascript'>");
html.append("var req;");
html.append("var cursor = -1;");
// Generate functions to support Ajax
html.append("function initializeXmlHttpRequest() {");
// Support for non-Microsoft browsers (and IE7+)
html.append("if (window.ActiveXObject) {");
// Support for Microsoft browsers
html.append("req=new ActiveXObject('Microsoft.XMLHTTP');");
html.append("}");
html.append("else {");
html.append("req=new XMLHttpRequest();");
html.append("}");
JspWriter out = pageContext.getOut();
try {
out.append(html.toString());
} catch (IOException e) {
e.printStackTrace();
}
return this.SKIP_BODY;
}
|
The
req variable can now be used globally within the Web
page. Listing 6 illustrates how to make an asynchronous
call.
Listing 6. Use the XMLHttpRequest object
// If req object initialized
if (req!=null) {
// Set callback function
req.onreadystatechange=stateName_onServerResponse;
// Set status text in browser window
window.status='Retrieving State data from server...';
// Open asynchronous server call
req.open('GET',dataUrl,true);
// Send request
req.send(null);
}
|
When
the ready state of a request changes, the function specified in req.onreadystatechange is invoked. req.readystate contains one of the following status codes:
- 0=initialized
- 1=Open
- 2=Sent
- 3=Receiving
- 4=Loaded
Normally, anything but Loaded is ignored because
typically nothing needs to occur until the server response is complete. A value of
Loaded for an asynchronous call doesn't guarantee
success. As with any Web page request, it's possible that the page wasn't found or
that another problem occurred. If the req.status is
anything but 200, something went wrong. Listing 7 shows how to handle the server
response.
Listing 7. Handle the response to the asynchronous request
function stateName_onServerResponse() {
if(req.readyState!=4) return;
if(req.status != 200) {
alert('An error occurred retrieving data.');
return;
}
// Obtain server response
var responseData = req.responseText;
... Processing of result
}
|
You've
now had a good overview of how to make asynchronous calls and handle responses. The
next step is to start building the first control: <ajax:autocomplete/>.
Build the auto-complete JSP TagLib control
The following steps are required to build the auto-complete control:
- Build a value provider to supply suggestions for the control.
- Create a Servlet interface to expose the value provider for asynchronous calls.
- Create a JSP TagLib control to encapsulate everything in a control that can be used in JSP pages.
The following sections explain these steps in detail.
Build the value provider to supply suggestions for the auto-complete control
The value provider
supplies the suggestion list to the auto-complete control. A value provider must
implement the IJsonValueProvider interface, which defines
a single method getValues() that returns a JSONArray object containing the suggestion list. The
interface is shown in Listing 8.
Listing 8. IJsonValueProvider interface
public interface IJsonValueProvider {
JSONArray getValues(String criteria, Integer maxCount);
}
|
The next step is to
create CityValueProvider, the implementation of this
interface, which provides city data for the <ajax:autocomplete/> control. Note the following key things about
the getValues() implementation:
- Data is retrieved from the Location Data Provider -- a Data Abstract Layer (DAL) component -- which caches all locations in memory.
- A two-phase approach is required to process the data (
TreeMapcontainingLocationDTOobjects), because the Location Data Provider returns aTreeMapsorted by zip code. The results need to be sorted based on the city name for theCityValueProvider.
Listing 9 illustrates how this is done.
Listing 9. City value provider
package com.testwebsite.bll;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeMap;
import org.json.JSONArray;
import com.testwebsite.dal.LocationDataService;
import com.testwebsite.dto.LocationDTO;
import com.testwebsite.interfaces.IJsonValueProvider;
/** @model
* @author Brian J. Stewart (Aqua Data Technologies, Inc. http://www.aquadatatech.com) */
public class CityValueProvider implements IJsonValueProvider {
/* (non-Javadoc)
* @see com.testwebsite.interfaces.IJsonValueProvider#getValues(java.lang.String)
*/
@Override
public JSONArray getValues(String criteria, Integer maxCount) {
String cityName = "";
// If city found, make the search case insensitive
if (criteria != null && criteria.length() > 0) {
cityName = criteria.toLowerCase();
}
// Get Location data from Data Provider
TreeMap<Integer, LocationDTO>
locData = LocationDataService.getLocationData();
// The LocationDataService Data Provider returns a TreeMap containing
// LocationDTO objects that are sorted by Zip Code.
// First build a temporary TreeMap (sorted list) filtering with
// only unique city names matching the specified cityName parameter
TreeMap<String, String> cityData = this.getCityData(locData, cityName);
// Finally iterate through sorted City list
// and create JSONArray containing
// the number elements specified by the maxCount parameter
JSONArray json = this.getJsonData(cityData, maxCount);
return json;
}
/**
* The getCityData method returns a TreeMap containing Cities matching the
* specified cityName criteria. The results are sorted by City Name and filter
* out any duplicate city names.
* @param locData Location Data from which to retrieve cities
* @param cityName City Name prefix to which to search
* @return
*/
protected TreeMap<String, String> getCityData(
TreeMap<Integer, LocationDTO> locData, String cityName) {
TreeMap<String, String> cityData = new TreeMap<String, String>();
// Iterate through all data looking for matching cities
// and add to temporary TreeMap
Set<Integer> keySet = locData.keySet();
Iterator<Integer> locIter = keySet.iterator();
while (locIter.hasNext()) {
// Get current state
Integer curKey = locIter.next();
LocationDTO curLocation = locData.get(curKey);
// Get current location data
if (curLocation != null) {
String curCityName = curLocation.getCity().toLowerCase();
// Add current item if it starts with the cityName parameter
if (curCityName.startsWith(cityName)) {
cityData.put(curLocation.getCity(),
curLocation.getCity());
}
}
}
return cityData;
}
/**
* The getJsonData method returns a JSONArray contain a list of strings
* with the city name specified with a maximum number of elements as specified
* by the maxCount parameter.
* @param cityData TreeMap containing unique list of matching cities
* @param maxCount Maximum number of items to include in the JSONArray
* @return JSONArray contain sorted list of city names
*/
protected JSONArray getJsonData(TreeMap<String, String>
cityData, int maxCount) {
int count = 1;
JSONArray json = new JSONArray();
// Get city name keys
Set<String> citySet = cityData.keySet();
// Iterate through query results
Iterator<String> cityIter = citySet.iterator();
while (cityIter.hasNext()) {
// Get current item
String curCity = cityIter.next();
// Add item to JSONArray
json.put(curCity);
// Increment counter
count ++;
// If maximum number of entries has been met, then exit loop
if (count >= maxCount) break;
}
return json;
}
}
|
Create a Servlet to handle asynchronous requests to the value provider
The next step is to build
the AutoCompleteServlet Servlet, the interface for the
Web browser to call the IJsonValueProvider
implementations. The Servlet is straightforward, with one minor exception. To meet
the goal of "easy integration/deployment," you should only need to worry about
implementing a value provider, rather than the Servlet interface. To support this
goal, you use reflection to instantiate the value provider at runtime using the
classname attribute of the <ajax:autocomplete/> control. See Listing
10.
Listing 10. Auto-complete Servlet
package com.testwebsite.servlets;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONArray;
/**
* @model
* @author Brian J. Stewart (Aqua Data Technologies, Inc. http://www.aquadatatech.com)
*/
public class AutoCompleteServlet extends HttpServlet {
/**
*
*/
private static final long serialVersionUID = -867804519793713551L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String data = "";
// Get parameters from query string
String format = req.getParameter("format");
String criteria = req.getParameter("criteria");
String maxCountStr= req.getParameter("maxCount");
String className = req.getParameter("providerClass");
// If format is not null and it's 'json'
if (format != null && format.equalsIgnoreCase("json")) {
if (className != null && className.length() > 0) {
data = this.getJsonResultAsString(criteria,
maxCountStr,
className);
}
resp.setContentType("text/plain");
}
// Write response
// Get writer for servlet response
PrintWriter writer = resp.getWriter();
writer.println(data);
writer.flush();
}
public String getJsonResultAsString(String criteria,
String maxCountStr,
String className) {
String data = "";
Integer maxCount = 10;
if (maxCountStr != null && maxCountStr.length() > 0) {
maxCount = new Integer(maxCountStr);
}
// Get dataprovider class using reflection
// Construct class
Class providerClass;
try {
// Get provider class
providerClass = Class.forName(className);
// Construct method and method param types
Class[] paramTypes = new Class[2];
paramTypes[0] = String.class;
paramTypes[1] = Integer.class;
Method getValuesMethod = providerClass.getMethod("getValues",
paramTypes);
// Construct method param values
Object[] argList = new Object[2];
argList[0] = criteria;
argList[1] = maxCount;
// Get instance of the provider class
Object providerInstance = providerClass.newInstance();
// Invoke method using reflection
JSONArray resultsArray = (JSONArray)
getValuesMethod.invoke(providerInstance,
argList);
// Convert JSONArray result to string
data = resultsArray.toString();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return data;
}
}
|
Figure 8 shows the server response from AutoCompleteServlet Servlet.
Figure 8. Auto-complete Servlet Response
Create a JSP TagLib control that can be used in a JSP page
The auto-complete control renders
a standard INPUT tag, sets up the event handlers, and
renders the Suggestion List Container DIV element and
appropriate CSS for formatting. You need to add the following supporting JavaScript
functions:
- Handle keyboard events --
<ajax:page/> - Handle server response and post-asynchronous invocation processing --
<ajax:autocomplete/>, with helper functions rendered in<ajax:page/> - Highlight a specified item in the suggestion list --
<ajax:page/> - Hide the suggestion list --
<ajax:page/> - Handle the selection of an item in the suggestion list (when the user
presses Enter) --
<ajax:page/> - Handle when the control loses focus --
<ajax:page/>
Let's start with the onSuggestionKeyDown
function, where the Esc key, Enter key, and other control keys are handled. If the
user presses Esc, the suggestion list should be hidden and subsequent events in the
JavaScript event chain canceled (for example, the Key Up event shouldn't be
processed because the event was already handled by hiding the suggestion list); see
Listing
11.
Listing 11. Code fragment for handling Esc key
var keyCode = (window.event) ? window.event.keyCode : ev.keyCode;
switch(keyCode) {
...
// Handle ESCAPE key
case 27:
hideSelectionList(curControl, suggestionList);
ev.cancelBubble = true;
// IE
if (window.event) {
ev.returnValue = false;
}
// Firefox
else {
ev.preventDefault();
}
break;
...
|
If
the user presses Enter, the current item should be copied to the input control and
the suggestion list hidden. To hide/display the suggestion list, you use standard
CSS for formatting in conjunction with JavaScript to change the class name. The
display property is set to none to hide the control and to block to display
the list. Listing 12 shows the JavaScript function, which
you add to the <ajax:page/> control because it can
be used with any <ajax:autocomplete/> control.
Listing 12. Code fragment for handling the Enter key
...
// Handle ENTER key
case 13:
handleSelectSuggestItem(curControl, suggestionList);
ev.cancelBubble = true;
// IE
if (window.event) {
ev.returnValue = false;
}
// Firefox
else {
ev.preventDefault();
}
break;
...
|
The
key-down event handler calls handleSelectSuggestItem,
which is defined in <ajax:page/> (see Listing
13).
Listing 13. Handling the Enter key
function handleSelectSuggestItem(curControl, suggestionList) {
// Get selected node
// Cursor is a global variable that is incremented/decremented
// when the UP ARROW or DOWN ARROW key is pressed.
var selectedNode = suggestionList.childNodes[cursor];
// Get selected value
var selectedValue = selectedNode.childNodes[0].nodeValue;
// Set the value of the INPUT control
curControl.value = selectedValue;
// Finally hide the selection list
hideSelectionList(curControl, suggestionList);
}
function hideSelectionList(curControl, suggestionList) {
// If suggestion not found
if (suggestionList == null || suggestionList == undefined) {
return;
}
// Clear the suggestion list elements
suggestionList.innerHTML='';
// Toggle display to none
suggestionList.style.display='none';
curControl.focus();
}
|
There isn't much to handling the pressing of control keys (Shift, Alt, and Ctrl). You need to ignore these keyboard events by doing the following:
- Preventing changes to the input control during the return by setting
returnValueon theEVENTobject tofalse(for Internet Explorer) and executingpreventDefault()on theEVENTobject (for Firefox) - Canceling the event chain for the keyboard event by setting the
cancelBubbleproperty on theEVENTobject totrue
The complete code for onSuggestionKeyDown
appears in Listing
14.
Listing 14. Complete key-down event handler
function onSuggestionKeyDown(curControl, ev) {
// Get suggestion list container
var suggestionList= document.getElementById(curControl.id + '_suggest');
// Get key code of key pressed
var keyCode = (window.event) ? window.event.keyCode : ev.keyCode;
switch(keyCode) {
// Ignore certain keys
case 16, 17, 18, 20:
ev.cancelBubble = true;
// IE
if (window.event) {
ev.returnValue = false;
}
// Firefox
else {
ev.preventDefault();
}
break;
// Handle ESCAPE key
case 27:
hideSelectionList(curControl, suggestionList);
ev.cancelBubble = true;
// IE
if (window.event) {
ev.returnValue = false;
}
// Firefox
else {
ev.preventDefault();
}
break;
// Handle ENTER key
case 13:
handleSelectSuggestItem(curControl, suggestionList);
ev.cancelBubble = true;
// IE
if (window.event) {
ev.returnValue = false;
}
// Firefox
else {
ev.preventDefault();
}
break;
}
}
|
The key-up event handler is slightly more interesting. If the user presses the Up Arrow or Down Arrow key, the highlighted selection should change. If the user has entered the minimum number of characters (default: 3), then the asynchronous call should be made to the server to populate the suggestion list.
If the user presses the Up
Arrow or Down Arrow key, the global cursor variable is
incremented/decremented accordingly. The cursor variable
tracks the currently selected item. The highlightSelectedNode function is than called to highlight the value. See Listing
15.
Listing 15. Code fragment of the key-up event handler for handling the Up Arrow and Down Arrow keys
...
switch(keyCode) {
// Ignore ESCAPE
case 27:
// Handle UP ARROW
case 38:
if (suggestionList.childNodes.length > 0 && cursor > 0){
var selectedNode = suggestionList.childNodes[--cursor];
highlightSelectedNode(suggestionList, selectedNode);
}
break;
// Handle DOWN ARROW
case 40:
if (suggestionList.childNodes.length > 0 &&
cursor < suggestionList.childNodes.length-1) {
var selectedNode = suggestionList.childNodes[++cursor];
highlightSelectedNode(suggestionList, selectedNode);
}
break;
...
|
Listing 16 shows the highlightSelectedNode function for highlighting an item. CSS rules are
defined for selected and unselected items. The className
is toggled using JavaScript. The highlight for the previously selected element is then
removed.
Listing 16. Highlight an item in the suggestion list
function highlightSelectedNode(suggestionList, selectedNode) {
if (suggestionList == null || selectedNode == null) {
return;
}
// Iterate through all items searching for a node that
// matches the node selected
for (var i=0; i < suggestionList.childNodes.length; i++) {
var curNode = suggestionList.childNodes[i];
if (curNode == selectedNode){
curNode.className = 'autoCompleteItemSelected'
} else {
curNode.className = 'autoCompleteItem';
}
}
}
|
If
the user presses any other key and has entered at least the minimum number of
characters, an asynchronous call to the server is made to retrieve a JSON array of
suggestions. After the ready state changes, the function specified in the req.onreadystatechange property is invoked (see Listing
17).
Listing 17. Code fragment for handling any other key
// If control not found (shouldn't happen)
// or minimum number of characters not entered
if (curControl == null ||
curControl.value.length < minChars) {
// Hide selected item
hideSelectionList(curControl, suggestionList);
return;
}
// Initialize XMLHttpRequest object
initializeXmlHttpRequest();
// If req object initialized
if (req!=null) {
// Set callback function
req.onreadystatechange=cityName_onServerResponse;
// Set status text in browser window
window.status='Retrieving State data from server...';
// Open asynchronous server call
req.open('GET',dataUrl,true);
// Send request
req.send(null);
}
|
When
the server-response function is invoked, the readyState
is checked to make sure it's Loaded. The status is also checked. If all is well, the string
representation of the JSON array is converted to an array using the eval JavaScript function. The array is then passed to the
populateSuggestionList function, which adds the
elements to the Suggestion List. Listing 18 shows the server-response function.
Listing 18. Server-response handler (generated dynamically by the
<ajax:autocomplete/> control)
function cityName_onServerResponse() {
// If loaded
if(req.readyState!=4) {
return;
}
// If an error occurred
if(req.status != 200) {
alert('An error occurred retrieving data.');
return;
}
// Get response and convert it to an array
var responseData = req.responseText;
var dataValues=eval('(' + responseData + ')');
// Get current control
var curControl = document.getElementById('cityName');
/// Populate suggestion list for control
populateSuggestionList(curControl, dataValues);
}
|
The
populateSuggestionList function, which is rendered in
the <ajax:page/> control, is responsible for
populating the suggestion list with the values returned from the asynchronous server
call. The array is then iterated through, and a DIV
element is created for each item in the array. The DIV
element is added to the Suggestion List. Listing 19 shows
populateSuggestionList.
Listing 19. Populate the suggestion list (rendered in the <ajax:page/> control)
populateSuggestionList(curControl, dataValues) {
// Get Suggest List Container for control
var container = document.getElementById(curControl.id + '_suggest');
// If container not found (shouldn't happen), then simply return
if (container == null) { return; }
// Clear suggestion list container
container.innerHTML = '';
// If no values return, hide suggestion list
if (dataValues.length < 1) {
container.style.display='none';
return;
}
// Show suggestion list
container.style.display='block';
container.style.top =
(curControl.offsetTop+curControl.offsetHeight) + 'px';
container.style.left = curControl.offsetLeft + 'px';
// Iterate through all values
// 1. Create DIV element
// 2. Set attributes and text node value
// 3. Append new element to the container
for(var i=0;i < dataValues.length;i++) {
// Get current value
var curValue= dataValues[i];
// If value is not blank
if (curValue != null && curValue.length > 0 ) {
// Create DIV element
var newItem = document.createElement('div');
// Append current value as a text node
newItem.appendChild(document.createTextNode(curValue));
// Set attributes
newItem.setAttribute('class', 'autoCompleteItem');
// Finally append new element to container
container.appendChild(newItem);
}
}
// Set first item as the selected node
cursor = 0;
// Get first node
var selectedNode = container.childNodes[cursor];
// If first node is equal to the first node, hide the selection list
if (selectedNode.childNodes[0].nodeValue == curControl.value) {
hideSelectionList(curControl, container);
}
else {
// Highlight the first node
highlightSelectedNode(container, selectedNode);
}
}
|
Auto-complete TagLib library definition entry
Listing 20 contains the auto-complete control's TagLib library definition entry (with embedded comments for a description of each attribute).
Listing 20. Auto-complete TagLib library definition entry
<tag>
<name>autocomplete</name>
<tagclass>com.testwebsite.controls.AutoCompleteTag</tagclass>
<bodycontent>JSP</bodycontent>
<info>
Auto-complete/suggest form input fields based on a specified value.
</info>
<!-- Unique identifier for control -->
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Minimum string length before submitting asynchronous request -->
<attribute>
<name>minimumlength</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Maximum number of items to include in suggestion list -->
<attribute>
<name>maxcount</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Width of control -->
<attribute>
<name>width</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Value of control -->
<attribute>
<name>value</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Data Url for asynchronous call. A default Servlet has been created,
but for greater flexibility, a Web Service or another Servlet can be
specified-->
<attribute>
<name>dataurl</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Class that provides suggest value list for control
(Used if dataUrl not specified -->
<attribute>
<name>providerclass</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
|
Build the cascading drop-down JSP TagLib control
Typically, business-line applications include selection lists whose values are dependent on other form fields (for example, a product name that depends on the product category).
Prior to Ajax and asynchronous Web programming techniques, you were forced to render all values to the Web page (typically in a JavaScript array) and to dynamically populate the values within JavaScript. The JavaScript arrays were either multidimensional or contained tokens, such as the | character to delimit cascading values. Alternatively, the entire page would be refreshed to retrieve the values for cascading selection lists. Neither of these approaches are appealing when you're dealing with large data sets or trying to build user-friendly Web applications. With Ajax and asynchronization techniques, you can now provide the same rich and intuitive user experience typically only found in desktop applications.
The following sections describe the steps to create the cascading drop-down control:
- Create a value provider/Servlet interface that can be called from JavaScript.
- Create the JSP TagLib control to encapsulate everything in a control that can be put in any JSP page.
Create the value provider and interface (Servlet)
Similar to the value provider you created for the auto-complete control, you create a Servlet to return a JSON array containing the values. Value providers for cascading controls take a little more effort, because the requirements and data often require separate Servlets or Web services to apply the business rules. Alternatively, you can use embedded JSP TagLib controls (controls included in the body of another tag), but that complicates things slightly. Using separate Servlets provides greater flexibility in terms of returning data to the client. The values can be dependent on other form fields or other complex business rules that are defined in the Servlet.
Listing 21 shows the two value providers for the cascading drop-down controls. The first is the City value provider, which is dependent on the State value. The second value provider is for County, which is dependent on the State and City values. Both Servlets return JSON arrays and use the Location Data Provider (a DAL component). The code is similar to developing a value provider for the auto-complete control; the key difference is that you use separate Servlets to keep the implementation simple and flexible.
Listing 21. CityServlet Servlet
package com.testwebsite.servlets;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.testwebsite.bll.LocationService;
public class CityServlet extends HttpServlet {
private static final long serialVersionUID = 3231866266466404450L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String data = null;
// Get parameters from query string
String format = req.getParameter("format");
String cityName = req.getParameter("cityName");
String stateName = req.getParameter("stateName");
// If format is not null and it's 'json'
if (format != null & format.equalsIgnoreCase("json")) {
// Get city data based on state name and city name prefix
data = LocationService.getCitiesAsJson(cityName, stateName);
resp.setContentType("text/plain");
}
// Write response
// Get writer for servlet response
PrintWriter writer = resp.getWriter();
writer.println(data);
writer.flush();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
super.doPost(req, resp);
}
}
/**
* @model
* @author Brian J. Stewart (Aqua Data Technologies, Inc. http://www.aquadatatech.com)
*
*/
public class CountyServlet extends HttpServlet {
/**
*
*/
private static final long serialVersionUID = 3231866266466404450L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String data = null;
// Get parameters from query string
String format = req.getParameter("format");
String cityName = req.getParameter("cityName");
String stateName = req.getParameter("stateName");
String countyName = req.getParameter("countyName");
// If format is not null and it's 'json'
if (format != null && format.equalsIgnoreCase("json")) {
data = LocationService.getCountiesAsJson(countyName,
stateName, cityName);
resp.setContentType("text/plain");
}
// Write response
// Get writer for servlet response
PrintWriter writer = resp.getWriter();
writer.println(data);
writer.flush();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
super.doPost(req, resp);
}
}
|
The cascading drop-down control works as follows:
- The page is rendered with an empty
SELECTcontrol. - The user selects the
SELECTcontrol. When it receives focus, an asynchronous call is made to retrieve the values from the server. - The server sends the JSON array of values back to the client.
- The client dynamically populates the
SELECTcontrol with the values in the JSON array. - After the user selects a value from the list and the control loses focus
(upon the
blurevent), the controls that are dependent on the current field are cleared. This is done to preserve data integrity (if the State value is change, the City value likely won't be valid).
The <ajax:page/> control renders the common
functions that are usable by all cascading drop-down controls on a page, and the
<ajax:dropdown/> control renders the JavaScript
specific for an individual control instance.
Render the SELECT control for the
cascading drop-down control
The rendering of the SELECT control is straightforward. The event handlers for the onfocus and onblur events are
rendered as shown in Listing
22.
Listing 22. Render the SELECT control for the cascading drop-down control
...
/**
* The getSelectControlHtml method returns the html code to render the drop down (html
* select) control.
* @return Html code for drop down (html select) control
*/
protected String getSelectControlHtml() {
StringBuffer html = new StringBuffer();
// Render dropdown/select control
html.append("<select id='");
html.append(this.getId());
// Render on focus event handler
html.append("' onfocus='");
html.append(this.getId());
html.append("_onSelect(this)'");
// Render on change event handler
html.append(" onChange='");
html.append(this.getId());
html.append("_onChange(this)'");
// Render css class if specified
if (this.getCssclass() != null && this.getCssclass().length() > 0) {
html.append(" class='");
html.append(this.getCssclass());
html.append("'");
}
// Render width if applicable (not 0/default/auto-fit)
if (this.getWidth() > 0) {
html.append(" style='width:");
html.append(this.getWidth());
html.append("px'");
}
html.append("/>");
return html.toString();
}
...
|
Event handlers for the cascading drop-down control
In the onSelect
event handler, the values for the controls, from which the current control cascades,
are retrieved, and the URL is built to send the asynchronous request to the server.
Upon receiving the response, the SELECT tag is populated
with values returned in the JSON Array using JavaScript (see Listing
23).
Listing 23. On-select event handler
function stateName_onSelect(curControl) {
if(curControl.options.length > 0) {
return;
}
clearOptions(curControl);
// Set waiting message in control
var waitingOption = new Option('Retrieving values...','',true,true);
curControl.options[curControl.options.length]=waitingOption;
// The dataUrl is built dynamically based on the cascadeTo control
var dataUrl = '/TestWebSite/State?format=json&stateName=' +
getSelectedValue('stateName');
// Initialize the XMLHttpRequest object
initializeXmlHttpRequest();
// If initialization was successful
if (req!=null) {
// Set callback function
req.onreadystatechange=stateName_onServerResponse;
// Set status text in browser window
window.status='Retrieving State data from server...';
// Open asynchronous server call
req.open('GET',dataUrl,true);
// Send request
req.send(null);
}
}
|
The
following things happen in the server-response handler CONTROL-NAME_onServerResponse, which is dynamically generated by the
cascading drop-down control tag:
- Ignore status changes unless
Loaded - Notify the user if an error occurred during the asynchronous call
- Get the current control, and clear all
OPTIONelements - Get the response data, and convert it to an array containing strings
- Populate the
SELECTcontrol using the array
Listing 24 is dynamically rendered by the <ajax:dropdown/> control.
Listing 24. Server-response handler (generated dynamically by the control)
function cityName_onServerResponse() {
// If not finished, then return
if(req.readyState!=4) {
return;
}
// If an error occurred notify user and return
if(req.status != 200) {
alert('An error occurred retrieving data.');
return;
}
// Get current control
var curControl = document.getElementById('cityName');
// Clear options
clearOptions(curControl);
// Get response data
var responseData = req.responseText;
// Convert to array
var dataValues=eval('(' + responseData + ')');
// Populate SELECT tag with OPTION elements
populateSelectControl(curControl, dataValues);window.status='';
}
|
The
populateSelectControl function, which is generated by
the <ajax:page/> tag, adds a blank OPTION to the SELECT control, as
well as an OPTION element for each value in the dataValues array. The dynamically generated code fragment
is shown in Listing
25.
Listing 25. Populate the SELECT control
function populateSelectControl(curControl, dataValues) {
// Append blank option
var blankOption= new Option('','',false,true);
curControl.options[curControl.options.length]=blankOption;
// Iterate through data value array
for (var i=0;i<dataValues.length;i++) {
// Create option
var newOption= new Option(dataValues[i],dataValues[i],false,false);
// Add option to control options
curControl.options[curControl.options.length]=newOption;
}
}
|
In
the onChange event handler, all controls that are
dependent on the current control are cleared (see Listing
26).
Listing 26. On-change event handler
function stateName_onChange(curControl) {
// Array dynamically generated by the control
var toList=['cityName','countyName'];
// If no controls are dependent on this function, simply return
if (toList == null || toList.length == 0) {
return;
}
// Iterate through list of controls that are dependent on
// the current control
for (var i=0; i < toList.length; i++) {
// Get current control name
var curControlName = toList[i];
// Get current control
var curToControl = document.getElementById(curControlName);
// If control not found, then exit
if (curToControl == null) return;
// Clear the current control
clearOptions(curToControl);
}
}
|
The
clearOptions function, which removes all items in the
parent SELECT control, is rendered in the <ajax:page/> control (see Listing
27).
Listing 27. On-change event handler
function clearOptions(curControl) {
// If current control is null then exit
if (curControl == null) {
alert('Unable to clear control');
return;
}
// Check if control is already blank and return if it is
if (curControl.options.length < 1) {
return;
}
// Clear the options
curControl.options.length = 0;
}
|
Cascading drop-down TagLib library definition entry
Listing 28 shows the cascading drop-down control's TagLib library definition entry (with embedded comments for a description of each attribute).
Listing 28. Cascading drop-down TagLib library definition entry
<tag>
<name>dropdown</name>
<tagclass>com.testwebsite.controls.DropDownTag</tagclass>
<bodycontent>empty</bodycontent>
<info>
Populates Drop Down control asynchronously cascading values.
</info>
<!-- Unique identifier for control -->
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Url for Value Provider -->
<attribute>
<name>dataurl</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Message displayed while retrieving values from Value Provider -->
<attribute>
<name>updatemessage</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- CSS class name -->
<attribute>
<name>cssclass</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Current control value-->
<attribute>
<name>value</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Comma separated list of control id from which the current
control cascades -->
<attribute>
<name>cascadefrom</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Comma separated list of control id to which the current control cascades -->
<attribute>
<name>cascadeto</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Width of control -->
<attribute>
<name>width</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
|
The next step is to build sample pages to test the Ajax-enabled
controls. You'll test the <ajax:autocomplete/>
control with the Create New Contact page, and you'll test the <ajax:dropdown/> control with the Create New Employee
page.
Figure 9 shows the test Create New Contact page, which demonstrates how the auto-complete control looks from a user perspective.
Figure 9. Create New Contact page demonstrates how to use the auto-complete control
The JSP code for this test page is shown in Listing 29.
Listing 29. Sample page to demonstrate using the auto-complete control
<%@ page contentType="text/html; charset=ISO-8859-5" %>
<%@ taglib prefix="ajax"
uri="/WEB-INF/tlds/ajax_controls.tld"%>
<html>
<head>
<title>New Contact Information</title>
<link href="core.css" rel="stylesheet"
type="text/css" />
<ajax:page/>
</head>
<body>
<div id="container">
<form>
<div class="dialog">
<div class="dialogTitle">
Contact Information
</div>
<div class="contentPane">
<div style="font-weight:bold">First Name:</div>
<div>
<input type="text" id="firstName"
size="40"/>
</div>
<div style="font-weight:bold">Last Name:</div>
<div>
<input type="text" id="lastName"
size="40"/>
</div>
<div style="font-weight:bold">Address:</div>
<div>
<input type="text" id="streetAddress"
size="40"/>
</div>
<div style="font-weight:bold">City:</div>
<div>
<ajax:autocomplete id="cityName" width="40"
providerclass="com.testwebsite.bll.CityValueProvider"/>
</div>
<div style="font-weight:bold">County:</div>
<div>
<input type="text" id="countyName"
size="40"/>
</div>
<div style="font-weight:bold">Zip Code:</div>
<div>
<input type="text" id="zipCode"
size="40"/>
</div>
</div>
<div class="buttonPane">
<input type="reset" />
<input type="submit" value="Save"/>
</div>
</div>
</form>
</div>
</body>
</html>
|
The City Name field is now Ajax enabled. As the user types text in the City Name field, suggestions are dynamically displayed, similar to Google's auto-suggest.
Figure 10 shows the Create New Employee page, which illustrates the cascading drop-down control from a user perspective.
Figure 10. Test page demonstrating the cascading drop-down control
The JSP code for this page is shown in Listing 30.
Listing 30. Sample page to demonstrate using the cascading drop-down control
<%@ page contentType="text/html; charset=ISO-8859-5" %>
<%@ taglib prefix="ajax" uri="/WEB-INF/tlds/ajax_controls.tld"%>
<html>
<head>
<title>New Employee</title>
<ajax:page/>
<link href="core.css" rel="stylesheet"
type="text/css" />
</head>
<body>
<div id="container">
<form>
<table class="dialog" cellspacing="0"
cellpadding="0">
<thead>
<tr>
<td class="dialogTitle" colspan="2">
Employee Information
</td>
</tr>
</thead>
<tbody>
<tr>
<td class="fieldLabel">
Last Name:
</td>
<td class="fieldValue">
<input type="text" id="lastName"
size="40"/>
</td>
</tr>
<tr>
<td class="fieldLabel">
First Name:
</td>
<td class="fieldValue">
<input type="text" id="firstName"
size="40"/>
</td>
</tr>
<tr>
<td class="fieldLabel">
Address:
</td>
<td class="fieldValue">
<input type="text" id="streetAddress"
size="40"/>
</td>
</tr>
<tr>
<td class="fieldLabel">
State:
</td>
<td class="fieldValue">
<ajax:dropdown id="stateName" dataurl="/State"
width="240"
updatemessage="Retrieving State data from server..."
cascadeto="cityName,countyName" />
</td>
</tr>
<tr>
<td class="fieldLabel">
City:
</td>
<td class="fieldValue">
<ajax:dropdown id="cityName" dataurl="/City"
updatemessage="Retrieving City data from server..."
cascadeto="countyName" width="240"
cascadefrom="stateName" />
</td>
</tr>
<tr>
<td class="fieldLabel">
County:
</td>
<td class="fieldValue">
<ajax:dropdown id="countyName" dataurl="/County"
updatemessage="Retrieving County data from server..."
cascadefrom="stateName,cityName" width="240"/>
</td>
</tr>
<tr>
<td class="fieldLabel">
Zip Code:
</td>
<td class="fieldValue">
<input type="text" id="zipCode"
size="40" />
</td>
</tr>
</tbody>
<tfoot align="right" class="buttonPane">
<tr>
<td colspan="2">
<input type="reset" />
<input type="submit" value="Save"/>
</td>
</tr>
</tfoot>
</table>
</form>
</div>
</body>
</html>
|
In this article, you learned a few asynchronous communication techniques and how you can add JSON and Ajax to business-line applications through reusable JSP TagLib controls. Business-line applications can significantly benefit from Ajax-based controls thanks to the improved user experience and more responsive and intuitive user interfaces. The code isn't tremendously complex; you just need to integrate the key blocks (JavaScript, CSS, and J2EE technologies) to build the Ajax-enabled JSP controls.
You can extend the controls further to do the following:
- Support cascading to/from other types of controls (in addition to SELECT controls)
- Add mouse-event handling to the auto-complete control
- Add encoding and checking for asynchronous requests
| Description | Name | Size | Download method |
|---|---|---|---|
| Contains all source code for this article1 | ArticleCodeSample.zip | 50KB | HTTP |
| Contains MySQL database scriptsfor this article2 | ArticleDatabaseScripts.zip | 50KB | HTTP |
Information about download methods
Notes
- This ZIP file contains all source code for the article.
- This ZIP file contains all MySQL database scripts.
Learn
- Learn more about JSON for Java, an open source
library for using JSON in Java.
- The
W3C Working Draft 15 April 2008 contains valuable information about the
proposed standard for XMLHttpRequest Object.
- Read an Introduction
to JSON.
- Refer to Mozilla's official
documentation for JavaScript.
Get products and technologies
-
The Zip Code Database Project is
a free zip code database.
- About.com: ZIP
Code Database is another free zip code database.





