Tuesday, September 27, 2005

Tutorial: Using Ajax in a JSF application - II

In part I, we have created a simple JSF application. Next we will implement Ajax into this application. This will require us to create i) a server side application to dynamically provide the data to the client and ii) client side javascript functions to request data and process the response.

Step 1: Create the servlet as a server side application.
Copy the following code and save it as AjaxServlet.java in ajax/Web-Inf/classes/demo folder:


package demo;


import javax.servlet.ServletConfig;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpSession;



public class AjaxServlet extends HttpServlet {

private ServletConfig servletConfig = null;

public void destroy() {

servletConfig = null;

}



public ServletConfig getServletConfig() {

return (this.servletConfig);

}



public String getServletInfo() {

return (this.getClass().getName());

}



public void init(ServletConfig servletConfig)

throws ServletException {

this.servletConfig = servletConfig;

}



public void doGet(HttpServletRequest request,

HttpServletResponse response)

throws java.io.IOException,

ServletException {

String key = (String)request.getParameter("key");

//get CountryBean object from ServletContext

CountryBean countryBean = (CountryBean)

getServletContext().getAttribute("CountryBean");

//get list of countries

String[] countries = countryBean.getCountries();

//find matches

String matches = getMatches(countries, key);

//print it out to the client

response.setContentType("text/xml");

java.io.PrintWriter out=response.getWriter();

out.print(matches);

out.flush();

}



public void doPost(HttpServletRequest request,

HttpServletResponse response)

throws java.io.IOException,

ServletException {

doGet(request, response);

}



private String getMatches(

String[] countries, String key){

//generate xml response

String cList = "<?xml version=\"1.0\" ";

cList += "encoding=\"UTF-8\" ?>";

cList +="<COUNTRIES>";

int count = 0;

//from countries list find first 5 matches

for(int i=0;i<countries.length;i++){

if(countries[i].toUpperCase().

startsWith(key.toUpperCase())){

cList += "<COUNTRY name=\"" + countries[i] + "\" />";

count++;

if(count == 5) break;

}

}

cList += "<TOTALCOUNT count=\"" + count + "\" />";

cList += "</COUNTRIES>";

return cList;



}



}



In doGet method of the above code, we first get the value of "key"
parameter submitted from the client. Next, we get the JSF managed
CountryBean from which we obtain a list of countries and store
it in a String array:

CountryBean countryBean = (CountryBean)
getServletContext().getAttribute("CountryBean");
//get list of countries
String[] countries = countryBean.getCountries();

We then call getMatches method to check for country names that start with the key string sent by the user. This is, of course, a very simple method but you can substitute it with your own advanced code.

Notice also that this method generates an XML document with a list of matching country names. Finally the XML document is sent to the client.

Step 2: Configure the Servlet
Open web.xml file from ajax/WEB-INF folder and add the custom servlet right after Faces Servlet:




<servlet>

<servlet-name>AjaxServlet</servlet-name>

<servlet-class>demo.AjaxServlet</servlet-class>

</servlet>



Add this servlet mapping after Faces Servlet mapping:



<servlet-mapping>

<servlet-name>AjaxServlet</servlet-name>

<url-pattern>/AjaxServlet</url-pattern>

</servlet-mapping>



Step 3: Add javascript code to the application

Download the script.js file from here and copy it to ajax/js folder.

Notice in the js file that the following code creates the XMLHTTPRequest object.
(Also notice the difference in the code for creating this object in IE vs Mozilla.)


var req;

//initialize the XMLHttpRequest object
if (window.XMLHttpRequest) {
req = new XMLHttpRequest();
} else if (window.ActiveXObject) {
req = new ActiveXObject("Microsoft.XMLHTTP");
}


The function initPopup is responsible for turning off the autocomplete
feature of the browser. It also gets the location of the autocomplete
text-box and positions the popup element right below it. We will call
this function after the page is loaded.

The function doMouseClick is called when an item is selected in
popup list. This function sets the item value to the text box.

The functions hidePopup and showPopup simply hide and show the
popup element.

The function getQuery uses the XMLHttpRequest object and submits
the current text-box value to the AjaxServlet. Notice the path to our custom AjaxServlet configured above and how the key value is appended to the URL:
var url = "/ajax/AjaxServlet?key=" + key;

Also, notice the following line in this function:
req.onreadystatechange = processResponse;

The above line indicates that the processResponse function is called
when the response is received by the client. The processResponse
function forwards the response to parseResponse function.

The parseResponse function processes the response from the server.
Remember that the response is an XML document. So this function parses the XML document using dom parser and extracts the country names.
The country names will then be added to the popup CSS element which will then be displayed by setting its visibility to true.

The following code is required for Mozilla since it does not implement loadXML method by default.


//check if it is IE
var isIE = (window.navigator.appName.toLowerCase().indexOf("microsoft")>=0);

//implement loadXML method for Mozilla since it is not supported
if(!isIE){
Document.prototype.loadXML = function (s) {

// parse the string to a new doc
var doc2 = (new DOMParser()).parseFromString(s, "text/xml");

// remove all initial children
while (this.hasChildNodes())
this.removeChild(this.lastChild);

// insert and import nodes
for (var i = 0; i < doc2.childNodes.length; i++) {
this.appendChild(this.importNode(doc2.childNodes[i], true));
}
};
}



Step 4: Reference the javascript code in JSP page


Add this line within the HEAD of jsp code:

<SCRIPT TYPE="text/javascript" LANGUAGE="JavaScript" SRC="js/script.js"></SCRIPT>

Step 5: Enable ajax for input text

Modify the inputText component as follows:

<h:inputText id="autoText" onblur="hidePopup()" onfocus="getQuery(this.value)" onkeyup="getQuery(this.value)" value="#SelectedCountryRecord.selectedCountry}" />

Notice that on-focus and on-key-up (i.e. whenever a key is pressed
and released), we call getQuery function to submit a new request
to the server to get updated list. On-blur, we hide the popup list.

Step 6: Create popup div element

Add this code right before the end tag for body:


<div align="left" class="box" id="autocomplete"
style="WIDTH:170px;visibility:hidden;
BACKGROUND-COLOR:#ccccff">
</div>
<SCRIPT TYPE="text/javascript" LANGUAGE="JavaScript">
initPopup('_id0:autoText','autocomplete');
</SCRIPT>


First we define the div element for popup. We then call initPopup javascript
function to size and position the popup element.

Tuesday, September 06, 2005

Tutorial: Using Ajax in a JSF application - I

This tutorial is intended to provide a hands-on experience on implementing an auto-complete Google suggest like TextBox in a JSF application. As mentioned before, we will not create a reusable custom textbox component but rather take a simple approach of using the standard JSF HTML Textbox. This means we will manage the necessary Javascript code in the web application on our own. The application being discussed here would be more useful for a web developer rather than a component developer.

As mentioned earlier, the application will contain a text box to enter a country name. As you type a new character in the text box, it will dynamically get upto 5 matching country names from the servlet and list those names for you to choose from. Once you select a country in the text box and submit the form, it will display a brief description of the selected country. The application itself is pretty simple but it intends to provide the following 2 key concepts:
1. How can we implement a Servlet in a JSF web application to interact with the browser in the client side as well as to interact with the JSF components in the server side to possibly update the server state?
2. How can we implement the Javascript code in the application for Ajax implementation in the client side?

In this part, we will create a simple JSF application without AJAX. In part II, we will extend the same application to implement AJAX.

Step 1: Set up the JSF application framework.

This tutorial is not an introduction to JSF. It is assumed that you already know how to create a JSF application. So the basic framework for this application is already provided. Download it from here and extract it to a folder in your computer. Notice the structure of the extracted files:

ajax
|-country.jsp
|-index.jsp
|-WEB-INF
|-faces-config.xml
|-web.xml
|-lib
|-classes
|-demo
|-country.data



Step 2. Add JSF libraries

Add the JSF jar files to ajax/Web-Inf/lib folder.

Step 3: Store the data.

To keep things simple, we will store the country data in a simple txt file separated by tab. In a real world application though, the data would be queried dynamically from the database, web services or some other sources.
As shown above, the data is already provided in ajax/web-inf/classes/demo/country.data file and is stored in this format:

Luxembourg Western Europe Europe Luxembourg LU
Switzerland Western Europe Europe Bern SZ
France Western Europe Europe Paris FR


Step 4: Read data.

We will read the data from the txt file when the application is initially started and keep it in the memory. We could have read the datasource dynamically for each client request. However, in this simple case, it makes more sense to load the data once at the startup.

Create the CountryBean.java file with the following content in ajax/Web-Inf/classes/demo folder:


package demo;

public class CountryBean{
String[] countries;
java.util.HashMap countryDetails = new java.util.HashMap();
public CountryBean(){
try{
//read country data from the text file
java.io.BufferedReader in=
new java.io.BufferedReader(
new java.io.InputStreamReader(
CountryBean.this.getClass().
getResourceAsStream("country.data"
)));
readData(in);
in.close();
} catch (java.io.IOException IOex) {
System.out.println("IO Error :" +IOex);
}
}

//returns country data
public java.util.HashMap getCountryDetails(){
return countryDetails;
}

//returns country list
public String[] getCountries(){
return countries;
}

//read data from the text file
//and store it in countryDetails
private void readData(java.io.BufferedReader br)
throws java.io.IOException {
int recordNum= Integer.parseInt(br.readLine());
countries = new String[recordNum];
for(int i=0; i<recordNum; i++){
String line=br.readLine();
java.util.StringTokenizer st =
new java.util.StringTokenizer(line, "\t\r");
String name= st.nextToken();
String region= st.nextToken();
String cont = st.nextToken();
String capital = st.nextToken();
String id = st.nextToken();
CountryRecord cr= new CountryRecord(name, region, cont, capital, id);
countryDetails.put(name, cr);
countries[i] = name;
}
}
}




The above class reads the data from the txt file. Notice that in the constructor,
we are calling readData method. In the readData method, we are reading each line from the text file and using the data to create a CountryRecord object to represent each country. The CountryRecord objects are then stored in a HashMap variable named countryDetails. We have also created another String array variable named countries and stored the country names in it.
The two methods getCountryDetails and getCountries simply return countryDetails and countries variables respectively so that they can be accessed from other places in the application.

Step 5: Add the class as a JSF managed bean

Open faces-config.xml from ajax/web-inf folder and add the above class as a managed bean as shown below:


<managed-bean>
<managed-bean-name>CountryBean</managed-bean-name>
<managed-bean-class>demo.CountryBean</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
</managed-bean>



Notice that we have added the bean with application scope because we want to
create a single instance of CountryBean throught the lifecycle of the application so that the database is read just once when the application starts.

Step 6: Create CountryRecord class

The CountryRecord class we used above is a simple class to store each of the country record. Create the CountryRecord.java file with the following content in ajax/Web-Inf/classes/demo folder:



package demo;
public class CountryRecord {
String name;
String region;
String continent;
String capital;
String id;

public CountryRecord(String name,
String region,
String continent,
String capital,
String id){
this.name=name;
this.region=region;
this.continent=continent;
this.capital = capital;
this.id=id;
}

public String getName(){
return name;
}

public String getRegion(){
return region;
}

public String getContinent(){
return continent;
}

public String getCapital(){
return capital;
}

public String getId(){
return id;
}
}



Step 7: Create a class to store the selected country information

Next we will create a class to store the state of the user selection. In this case, it is the Selected Country Record. Create the SelectedCountryRecord.java file with the following content in ajax/Web-Inf/classes/demo folder:


package demo;
public class SelectedCountryRecord{
String selectedCountry;
CountryRecord selectedCountryRecord;
java.util.HashMap countryDetails;

public SelectedCountryRecord(){
setCountryDetails();
}

public String getSelectedCountry(){
return selectedCountry;
}
public void setSelectedCountry(String selectedCountry){
this.selectedCountry = selectedCountry;
setSelectedCountryRecord();
}

public String getName(){
if(selectedCountryRecord != null)
return selectedCountryRecord.getName();
else return null;
}
public String getRegion(){
if(selectedCountryRecord != null)
return selectedCountryRecord.getRegion();
else return null;
}
public String getContinent(){
if(selectedCountryRecord != null)
return selectedCountryRecord.getContinent();
else return null;
}
public String getCapital(){
if(selectedCountryRecord != null)
return selectedCountryRecord.getCapital();
else return null;
}
public String getId(){
if(selectedCountryRecord != null) {
if (selectedCountryRecord.getId() != "") {
String id = "http://www.cia.gov/cia/publications/factbook/geos/"
+ selectedCountryRecord.getId().toLowerCase() + ".html";
return id;
}
}
return null;
}
public boolean isSelected(){
if(selectedCountryRecord != null) return true;
else return false;
}
public void setSelectedCountryRecord(){
if(selectedCountry != null) {
Object obj = countryDetails.get(selectedCountry);
if((obj != null) &&
(obj instanceof CountryRecord)) {
selectedCountryRecord = (CountryRecord) obj;
}
else selectedCountryRecord=null;
} else selectedCountryRecord = null;
}

public void setCountryDetails(){
javax.faces.context.FacesContext context =
javax.faces.context.FacesContext.getCurrentInstance();
javax.faces.application.Application app =
context.getApplication();
CountryBean cBean = (CountryBean)
app.createValueBinding("#{CountryBean}").getValue(context);
countryDetails = cBean.getCountryDetails();
}
}



The constructor of this class calls setCountryDetails() method:

public SelectedCountryRecord(){
setCountryDetails();
}


In the setCountryDetails method, we first get the Application object from FacesContext.
Next, we will get the JSF managed CountryBean that we added above using JSF value binding expression. From the CountryBean, we will get the CountryDetails variable. Remember that this is a hashmap variable which stores the detail information of all countries.

public void setCountryDetails(){
javax.faces.context.FacesContext context =
javax.faces.context.FacesContext.getCurrentInstance();
javax.faces.application.Application app =
context.getApplication();
CountryBean cBean = (CountryBean)
app.createValueBinding("#{CountryBean}").getValue(context);
countryDetails = cBean.getCountryDetails();
}

The class level variables selectedCountry and selectedCountryRecord store the
user selected country's name and CountryRecord object respectively. Here are the get and set methods for selectedCountry variable:

public String getSelectedCountry(){
return selectedCountry;
}
public void setSelectedCountry(String selectedCountry){
this.selectedCountry = selectedCountry;
setSelectedCountryRecord();
}

Notice that the set method calls setSelectedCountryRecord() method in addition
to setting the new value. We are calling setSelectedCountryRecord method here because as the user enters a new value for country, we will update the value of selectedCountryRecord too for the new country.

public void setSelectedCountryRecord(){
if(selectedCountry != null) {
Object obj = countryDetails.get(selectedCountry);
if((obj != null) && (obj instanceof CountryRecord)){
selectedCountryRecord = (CountryRecord) obj;
} else selectedCountryRecord=null;
} else selectedCountryRecord = null;
}

In the above method, we are first getting the new CountryRecord object from
the hashMap by passing the new selectCountry value :

Object obj = countryDetails.get(selectedCountry);

and then we are updating the selectedCountryRecord variable:

selectedCountryRecord = (CountryRecord) obj;

The other methods getName(), getCapital(), getRegion(), getContinent(), getId()simply return the name, capital, region, continent and id respectively from the selectedCountryRecord variable. We will bind these methods with the JSP outputText components to display those values.

Step 8: Add SelectedCountryRecord as a managed bean

Open faces-config.xml from ajax/web-inf folder and add the above class as a
managed bean as shown below:

<managed-bean>
<managed-bean-name>SelectedCountryRecord</managed-bean-name>
<managed-bean-class>demo.SelectedCountryRecord</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>

Notice that this time we have added the bean with the session scope unlike
the previous one. This is because this class contains the data of the selected country for a particular user session. Hence, we need to add it with session scope to create a separate instance to store the individual state of each user session.

Step 9: Compile the above 3 classes

Put the JSF and Servlet libraries in class path and compile the above classes.

Step 10: Create the JSP page

Add this content to ajax/country.jsp file:


<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<html>
<head>
<title>Country Details</title>
</head>
<body>
<f:view>
<h:form>
<h:outputText
value="Integrating AJAX in a simple JSF Application"
styleClass="title"/>
<br>
<br>
<h:outputText value="Enter Country : " />
<h:inputText id="autoText"
value="#{SelectedCountryRecord.selectedCountry}" />
<h:commandButton value="OK" action="success"/>
<hr>

<table>
<tr>
<td>
<h:outputText value="Country Details"
rendered="#{SelectedCountryRecord.selected}"/>
</td>
</tr>
<tr>
<td>
<h:outputText value="Name : "
rendered="#{SelectedCountryRecord.selected}"/>
<h:outputText value="#{SelectedCountryRecord.name}" />
</td>
</tr>
<tr>
<td>
<h:outputText value="Region : "
rendered="#{SelectedCountryRecord.selected}"/>
<h:outputText value="#{SelectedCountryRecord.region}" />
</td>
</tr>
<tr>
<td>
<h:outputText value="Continent : "
rendered="#{SelectedCountryRecord.selected}"/>
<h:outputText value="#{SelectedCountryRecord.continent}" />
</td>
</tr>
<tr>
<td>
<h:outputText value="Capital : "
rendered="#{SelectedCountryRecord.selected}"/>
<h:outputText value="#{SelectedCountryRecord.capital}" />
</td>
</tr>
<tr>
<td>
<h:outputText value="More Descriptions : "
rendered="#{SelectedCountryRecord.selected}"/>
<h:outputLink value="#{SelectedCountryRecord.id}">
<h:outputText value="#{SelectedCountryRecord.id}" />
</h:outputLink>
</td>
</tr>
</table>
</h:form>
</f:view>

</body>
</html>


The above jsp page is pretty simple. Notice that we have binded the value of
inputText (to enter the country name) with selectedCountry variable in SelectedCountryRecord bean. The values of outputTexts (to display country information) are binded with the corresponding methods in SelectedCountryRecord bean to return the relevant information for the selected country.