内容


构建启用了 Ajax 的 JSP TagLib 控件,第 2 部分

自动填充和字段验证器控件

使用 JSP TagLib、JSON 和 Ajax

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 构建启用了 Ajax 的 JSP TagLib 控件,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:构建启用了 Ajax 的 JSP TagLib 控件,第 2 部分

敬请期待该系列的后续内容。

简介

下一代 Web 站点的开发依赖于两个关键技术:Asynchronous JavaScript + XML(Ajax)和 JavaScript Serialized Object Notation(JSON)。业务应用程序能从这些技术中受益以提供更直观、响应性更好的用户界面。本文介绍了如何通过构建可重用的基于 Ajax 的 JavaServer Pages (JSP) TagLib 控件向 Java™ Platform Enterprise Edition(Java EE)Web 应用程序中添加 Ajax 和 JSON。

在本文中,我展示了如何构建一个自动填充控件来基于在 HTML INPUT 字段内所输入的值动态填充表单字段。我还会介绍如何构建一个启用了 Ajax 的服务器验证器控件,该控件通过对数据验证服务器执行异步调用验证 HTML 控件的值。服务器验证器支持单字段验证、复合字段验证(名和姓)以及多反馈机制,比如动态 HTML(DHTML)和 JavaScript 报警。这些控件可通过集成 JSON、JavaScript、CSS、 HTML 和 Java EE 技术构建。

关于本系列

本文是由多部分组成的系列文章的其中之一,该系列文章介绍了如何为 J2EE 应用程序开发启用了 Ajax 的控件套件。这些控件均使用 JSP TagLib 控件构建并综合使用了 JSON、Servlet 和 JavaScript 技术。

关于本文

本文展示了如何构建如下控件:

  • 自动填充— 当在字段内输入值时,将自动填充 HTML 表单上的字段。比如,如果用户输入了一个帐号,用户的名称和地址信息就会从服务器异步检索到,而且 HTML 表单字段也会相应更新。
  • 服务器字段验证器— 向 HTML 表单字段添加服务器端验证。与向服务器提交整个字段并执行验证不同,字段数据是使用 JSON 和 JavaScript 脚本语言通过异步服务器调用实时验证的。这就使得用户界面更加直观、响应性更好。

这两种 JSP TagLib 控件均封装了所有的异步通信、JavaScript 代码、CSS 格式化以及 HTML 生成。

技术概览

本文构建于本系列 第一篇 文章中描述的设计原则和代码的基础之上。

正如在前一篇文章中所述的,启用了 Ajax 的 JSP 控件套件的主要设计目标是:

  • 提供与现有 Web 应用程序的简便集成 — 控件应封装所有逻辑和 Java 代码以简化部署过程
  • 应是可配置的
  • 数据及页面大小的负荷应该最少
  • 应利用 CSS 和 HTML 标准
  • 提供跨浏览器支持(Windows® Internet Explorer®、 Mozilla Firefox)
  • 应利用常见的设计模式/最佳实践来提高代码的可维护性

为了实现简便集成和可配置控件的设计目标,在必要时,使用了可配置标记属性。此外,还定义了接口/协议以便提供一种直观的方式来集成定制数据/值提供程序和这些控件。

我们使用了额外一种控件来封装常见的 JavaScript 函数以最小化数据和负荷。此外,还使用了 JSON 来最小化进行异步调用时的数据交换。

为了提供跨浏览器的支持,使用了包括 CSS 和 HTML 在内的 Web 标准。这些控件所涉及的 JavaScript 代码、HTML 和 CSS 均针对 Internet Explorer 7.x 和 Mozilla FireFox 2.x/3.x 进行了测试。

数据和值提供程序的构建所基于的是常见的面向对象编程(OOP)设计模式和最佳实践,比如 n-层架构、适配器设计模式和基于界面编程。

控件实现的技术考虑

要开发本文中展示的启用了 Ajax 的控件,有几个关键的技术方面需要考虑,比如为 Ajax 控件提供值的机制、异步通信的数据交换格式、类的设计及数据模型。

为异步调用提供响应的机制

与第一篇文章相同,出于效率和减少负荷方面的考虑,本文也使用了 Servlet。一个 JSP 页面较 Servlet 更容易实现,但从实现角度而言,前一种方式并不是很清晰。参见早期文章(技术考虑 — 为异步调用提供响应的机制)获得有关基本原理及其他选择的更多信息。

数据交换格式方面的考虑

与第一篇文章相同,由于 JSON 的负荷较小并具备性能方面的优势,本文使用了 JSON 作为数据交换的格式。参见前面的文章(技术考虑 — 数据交换格式方面的考虑)获得有关基本原理及其他选择的更多信息。

数据模型

此示例应用程序的数据模型由三部分组成:

  • 状态(state),包含状态缩写和名称
  • 位置(location),包含城市、邮编及其他位置数据
  • 帐号(account),包含已注册的用户数据

图 1 显示了本文中的示例页面所用的数据模型。

图 1. 数据模型
数据模型
数据模型

类模型

本文的示例包含 Data Abstract Layer (DAL)、Data Transfer Objects (DTO)、Business Logic Layer (BLL) 和 Presentation Layer 以及帮助程序类。下图所展示的是针对这些类的 Unified Modeling Language (UML) 类图。

本文使用了本系列第一篇文章中的 JdbcQueryJSHelper 帮助程序类。不需要其他的实用工具类。本文还使用了第一篇文章中的 LocationDataService。新创建的 AccountDataService DAL 类如图 2 所示。

图 2. UML 类图 — DAL 类
UML 类图 —— DAL 类
UML 类图 —— DAL 类

本文没有创建额外的 DTO 类。仍使用第一篇文章中的 LocationDTO

BLL 包含值 提供程序来为启用了 Ajax 的控件提供数据,它还包含服务器端验证器来为 HTML 表单数据提供服务器验证。本文使用了第一篇文章中的 LocationService 类。此外,还会创建两个新的业务类:AccountNameValidatorNameValidator。图 3 给出了这两个类。

图 3. UML 类图 — BLL 类
UML 类图 —— BLL 类
UML 类图 —— BLL 类

这些 Servlet 负责提供接口,<ajax:autopopulate/><ajax:servervalidator/> 控件向此接口做异步请求。AutoPopulateServlet<ajax:autopopulate/> 控件使用。ServerValidatorServlet<ajax:servervalidator/> 控件使用。这两个类如图 4 所示。

图 4. UML 类图 — BLL,Servlet 类
UML 类图 —— BLL,Servlet 类
UML 类图 —— BLL,Servlet 类

此自动填充控件包含 <ajax:autopopulate/><ajax:populaterule/> 标记。服务器端控件包含 <ajax:servervalidator/><ajax:validatorargument/> 标记。这四个类如图 5 所示。

图 5. UML 类图 — JSP TagLib 控件类
UML 类图 —— Business Logic Layer,Servlet 类
UML 类图 —— Business Logic Layer,Servlet 类

构建数据提供程序和数据层

LocationDataService 类返回包含位置相关数据的 LocationDTO 对象的 TreeMap。此类被用于 <ajax:autopopulate/> 示例。

<ajax:servervalidator/> 示例使用了 AccountDataService 类来检查帐户名或名/姓组合是否惟一。AccountDataService 类如清单 1 所示。

清单 1. AccountDataService
package com.testwebsite.dal;

import java.sql.ResultSet;
import java.sql.SQLException;
import com.testwebsite.util.JdbcQuery;

public class AccountDataService {	
	/**
	 * The getQueryToCheckAccount method returns the query string
	 * _cnnew1@param loginId
	 * @return
	 */
	private String getQueryToCheckAccountName(String loginId) {		
		StringBuffer queryString = new StringBuffer();
		queryString.append( "SELECT count(*) as rowcount FROM 
			test.account WHERE lower(login_id)='");
		queryString.append(loginId.toLowerCase());
		queryString.append("'");		
		return queryString.toString();
	}
	
	/**
	 * The isAccountUnique method returns true/false if login id is valid / unique
	 * @param loginId Login id of account
	 * @return True/false if login id is valid
	 */
	public boolean isAccountUnique(String loginId) {
		boolean isOk = false;
	
		// Check arguments
		if (loginId == null || loginId.length() ==0) { return isOk;	}
		
		// Get query string 
		String queryString = this.getQueryToCheckAccountName(loginId);		
		ResultSet results = JdbcQuery.getResults(queryString);
		
		// If no data was found then return empty data set
		if (results == null) { return isOk; }
		
		try {
			if (results.next()) {
				int count = results.getInt(1);
				isOk = (count == 0);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		return isOk;
	}
	
	/**
	 * The isAccountUnique method returns true/false if login id is valid / unique
	 * @param firstName First Name to check
	 * @param lastName Last Name to check
	 * @return True/false if login id is valid
	 */
	public boolean isNameUnique(String firstName, String lastName ) {
		boolean isOk = false;
	
		// Check arguments
		if (firstName == null || firstName.length() ==0 ||
		lastName == null || lastName.length() ==0) { return isOk; }
		
		// Get query string 
		String queryString = this.getQueryToCheckName(firstName, lastName);	
		ResultSet results = JdbcQuery.getResults(queryString);
		
		// If no data was found then return empty data set
		if (results == null) { return isOk; }
		
		try {
			if (results.next()) {
				int count = results.getInt(1);
				isOk = (count == 0);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		return isOk;
	}

	private String getQueryToCheckName(String firstName, String lastName) {
		StringBuffer queryString = new StringBuffer();
		queryString.append( "SELECT count(*) as rowcount FROM 
			test.account WHERE lower(last_name)='");
		queryString.append(lastName.toLowerCase());
		queryString.append("' AND lower(first_name)='");
		queryString.append(firstName.toLowerCase());
		queryString.append("'");
		
		return queryString.toString();
	}		
}

构建 JSP TagLib 自动填充控件

构建这个自动填充控件需要如下步骤:

  1. 构建表单数据提供程序来为此控件提供表单数据。
  2. 开发一个 Servlet 来为异步调用公开此数据提供程序。
  3. 创建一个 JSP TagLib 控件来封装此自动填充控件。
  4. 创建一个 JSP TagLib 控件用于填充规则。

下面的小节对这些步骤进行了详细的解释。

为自动填充控件构建表单数据提供程序

这个表单数据提供程序为此自动填充控件提供表单字段数据。一个表单数据提供程序必须实现 IJsonFormDataProvider 接口,该接口定义了单个方法 getFormData。此方法返回可由 Web 浏览器使用 JavaScript 代码处理的 JSONObject。清单 2 展示了这个表单数据提供程序接口。

清单 2. IJsonFormDataProvider 接口
public interface IJsonFormDataProvider {
	/**
	 * Returns a JSONObject of the data for the specified criteria. The maximum
	 * (maxCount) number of values is retrieved.
	 * @param criteria Criteria for which to search
	 * @return JSONObject for specified criteria
	 */
	JSONObject getFormData(String criteria);
}

下一步是创建一个示例表单数据提供程序类来实现此 IJsonFormDataProvider 接口。LocationFormDataProvider 类提供了位置相关数据来获得特定的邮政编码值。LocationFormDataProvider 类如清单 3 所示。

清单 3. LocationFormDataProvider
package com.testwebsite.bll;

import org.json.JSONObject;
import com.testwebsite.interfaces.IJsonFormDataProvider;

public class LocationFormDataProvider implements IJsonFormDataProvider {

	@Override
	public JSONObject getFormData(String criteria) {
		return LocationService.getLocationAsJson(criteria);
	}
}

LocationFormDataProvider 服务使用 LocationService 类为这个自动填充控件提供与位置相关的数据。LocationService 类从 TreeMap 检索这个位置 DTO,LocationDTOLocationService 类如清单 4 所示。

清单 4. LocationService
package com.testwebsite.bll;

import java.util.Iterator;
import java.util.Set;
import java.util.TreeMap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.testwebsite.dal.LocationDataService;
import com.testwebsite.dto.StateDTO;
import com.testwebsite.dto.LocationDTO;

/**
 * Provides values for Auto-complete and Location related Servlets
 */
public class LocationService {
	
	/**
	 * Returns Location for the specified zip code.
	 * @param Zip code criteria to retrieve
	 * @return JsonObject containing location data 
	 * for the specified zip code.
	 */
	public static JSONObject getLocationAsJson(String criteria) {
		if (criteria == null) return null;
		
		TreeMap<Integer, LocationDTO> locData = 
			LocationDataService.getLocationData();
			
		JSONObject locationJson = new JSONObject();
		try {
			Integer curKey = Integer.parseInt(criteria);
			LocationDTO curLocation = locData.get(curKey);
			
			if (curLocation != null) {
				String cityName = curLocation.getCity();
				String countyName = curLocation.getCounty();
				String stateName = curLocation.getState();
				
				locationJson.put("stateName", stateName);
				locationJson.put("zipCode", criteria);
				locationJson.put("cityName", cityName);
				locationJson.put("countyName", countyName);
			}
		} catch (JSONException e) {
			e.printStackTrace();
		}
		
		return locationJson;
	}
}

开发一个 Servlet 来为异步调用公开此数据提供程序

下一步是构建 AutoPopulateServlet Servlet,它是 Web 浏览器调用这些 IJsonFormDataProvider 实现的接口。这个 Servlet 很直观,只不过为了满足简便集成/部署的目标,使用了反射(reflection)来在运行时实例化此表单数据提供程序,并且此类在 <ajax:autopopulate/> 控件的 providerclass 属性指定。这种实现方法让您能将精力集中于基于业务的特定要求而非通信框架来实现表单数据提供程序。AutoPopulateServlet 如清单 5 所示。

清单 5. AutoPopulateServlet
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;
import org.json.JSONObject;

public class AutoPopulateServlet 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 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, 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 className) {
		String data = "";
		
		// 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[1];
			paramTypes[0] = String.class;
			Method getValuesMethod = 
				providerClass.getMethod("getFormData", paramTypes);
			
			// Construct method param values
            Object[] argList = new Object[1];            
            argList[0] = criteria;
            
            // Get instance of the provider class
            Object providerInstance = providerClass.newInstance();
			
            // Invoke method using reflection
            JSONObject result = (JSONObject) 
				getValuesMethod.invoke(providerInstance, argList);
			
			// Convert JSONObject result to string
			data = result.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;	
	}
}

服务器通过向 Web 服务器返回包含表单数据的 JSONObject 进行响应。图 6 展示了来自 AutoPopulateServlet Servlet 的一个示例服务器响应:

图 6. 自动填充 Servlet 响应
自动填充 Servlet 响应
自动填充 Servlet 响应

构建自动填充标记

这个自动填充控件需呈现一个标准 INPUT 并设置事件处理程序。因此需要添加如下 JavaScript 支持函数:

  • 处理 blur 来执行异步服务器调用
  • 处理异步服务器响应
  • 禁用表单字段(包含在 <ajax:page/> 内)

先来看看 onBlur 函数,异步服务器调用在该函数内构建。清单 6 所示的 onBlur JavaScript 函数由 <ajax:autopopulate/> 控件生成,可构建此异步请求的 URL。

清单 6. onBlur 函数(由 <ajax:autopopulate/><ajax:autopopulaterule/> 控件动态生成)
function zipCode_onBlur(curControl) {
		// If curControl is not null or blank
		if (curControl.value == null || curControl.value == '') 
			return;
			
		// Dynamically built target url
		var dataUrl = '/ajaxcontrols2/AutoPopulateData?
			providerClass=com.testwebsite.bll.LocationFormDataProvider&
			format=json&criteria=' + curControl.value;
		
		// Initialize asynchronous request
		initializeXmlHttpRequest();
		
		// Perform request
		if (req!=null) {
			req.onreadystatechange=zipCode_onServerResponse;
			window.status='Retrieving data from server...';
			req.open('GET',dataUrl,true);
			
			req.send(null);
		}
}

onServerResponse JavaScript 函数由 <ajax:autopopulate/><ajax:populaterule/> 控件生成,负责处理服务器响应。

当调用服务器响应函数时,会相应检查 readyState 以确保它是 Loaded。此外,还会检查 status 以了解是否有任何服务器错误发生。若 readyState 不是 Loadedstatus 包含错误,JavaScript 函数则不会发出动作,继而退出。如果一切顺利,JSON 对象的字符串表示就会通过 eval JavaScript 函数转变成一个 JavaScript 对象。

随后将再针对每个表单字段(包含在 <ajax:autopopulate/> 内的 <ajax:populaterule/> 标记)执行如下动作:

  • 检索相应的表单字段引用。
  • 检查目标字段是否存在。
  • 从 JSON 对象获得目标表单字段值。
  • 将表单字段设置为在返回的 JSON 对象内指定的值。
  • 禁用表单字段(如果适用)。

清单 7 给出了这个服务器响应函数。

清单 7. onServerResponse 函数(由 <ajax:autopopulate/><ajax:populaterule/> 控件动态生成)
function zipCode_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 object
	var responseData = req.responseText;
	var dataObj =eval('(' + responseData + ')');
	
	if (dataObj == null) { 
		return; 
	}
	
	// ***Start of dynamically generated based on populate rules***
	// Get form field
	var cityNameField = document.getElementById('cityName');
	
	// If form field not found
	if (cityNameField == null) {
		alert('cityName not found');
	}
	
	// Get form field value from data response
	var cityNameValue = dataObj.cityName;

	// Set form field
	cityNameField.value = cityNameValue;
	
	// Disable form field
	disableFormField('cityName', true);
	// ***End of dynamically generated by populate rule***
	
	...
	
	// Clear status bar
	window.status='';
}

disableFormField JavaScript 函数(如果适用)在 <ajax:page/> 内呈现并由 onServerResponse 函数调用,它接受两个参数:controlNameisDisabledcontrolName 参数包含要更新的控件的名称。isDisabled 参数根据字段是否禁用而被设为 true/false。如果向第二个参数传递 null,那么此参数默认的值就是 true(禁用字段)。JavaScript 代码非常直观,如果此字段被检索到,则禁用此字段。清单 8 给出了这个 disableFormField 函数。

清单 8. disableFormField 函数(由 <ajax:page/> 控件动态生成)
function disableFormField(controlName, isDisabled) {
	// If isDisabled is null then assume true
	if (isDisabled == null) isDisabled = true;
	
	// If control name is null return
	if (controlName == null) 
		return;
	
	// Fetch the control
	var curControl = document.getElementById(controlName);
	
	// If control not found return
	if (curControl == null) 
		return;
	
	// Disable form field
	curControl.disabled = isDisabled;
}

自动填充 TagLib 库定义项

下一步是在 TagLib 库定义文件内定义 <ajax:autopopulate/> JSP 控件。自动填充控件的 TagLib 库定义项(具有针对每个属性描述的内嵌注释)如清单 9 所示。

清单 9. 自动填充 TagLib 库定义项
<tag>
	<name>autopopulate</name>
	<tagclass>com.testwebsite.controls.autopopulate.AutoPopulateTag
	</tagclass>
	<bodycontent>JSP</bodycontent>
	<info>
		Auto-populate form input fields based on a specified value.
	</info>
	<!-- Unique identifier for control -->
	<attribute>
		<name>id</name>
		<required>true</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- Class that provides form field data for control -->
	<attribute>
		<name>providerclass</name>
		<required>true</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- Message displayed when asynchronous call is made -->			
	<attribute>
		<name>updatemessage</name>
		<required>false</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- True/false to disable target fields upon update -->
	<attribute>
		<name>disablefields</name>
		<required>false</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>		
</tag>

填充规则 TagLib 库定义项

下一步是在 TagLib 库定义文件内定义 <ajax:populaterule/> JSP 控件。自动填充控件的 TagLib 库定义项(具有针对每个属性描述的内嵌注释)如清单 10 所示。

清单 10. 填充规则 TagLib 库定义项
<tag>
	<name>populaterule</name>
	<tagclass>com.testwebsite.controls.autopopulate.PopulateRuleTag
	</tagclass>
	<bodycontent>empty</bodycontent>
	<info>Contains the auto-populate rule for form fields.</info>
	<!-- Unique identifier for control -->
	<attribute>
		<name>sourceattribute</name>
		<required>true</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<attribute>
		<name>fieldname</name>
		<required>true</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
</tag>

构建服务器验证器 JSP TagLib 控件

很多业务应用程序都有基于业务规则验证表单字段的要求。每个业务规则都是惟一的,如下所示的是几个业务验证规则的例子:

  • 确保帐号名惟一。
  • 确保名和姓的组合惟一。
  • 确保表单数据与现有的数据记录不冲突。

这些业务规则都要求进行服务器端处理(即数据库查找)。在 Ajax 出现之前,表单需要提交并要在服务器端进行处理。如果有错误发生,就会通知用户,但通常由于需要服务器处理,通知会在很长一段延迟之后才发出。从用户角度看来,这很不理想,因为用户更喜欢输入数据时能得到实时反馈。有了 Ajax,服务器端验证可以通过异步调用实现,用户也可以得到近乎实时的通知(在收到异步响应之后)。

<ajax:servervalidator/> 在使用 onBlur 事件失去焦点时验证目标控件。对服务器进行异步调用以执行服务器验证。

<ajax:servervalidator/> 提供如下的可配置选项:

  • targetfield— 被验证的目标字段的名称
  • updatemessage— 在异步服务器验证调用期间显示状态消息(比如,“Please Wait...”)
  • validatorclass— 服务器端 Java 类名(包含这个包的完全限定类名)
  • displaymode— 用户通知模式:JavaScript 警告或 DHTML
  • clearinvalid— 输入无效值时,清空此目标表单字段
  • refocusinvalid— 输入无效值时,重新聚焦此目标表单字段

<ajax:servervalidator/> 控件也可以包含一个或多个 <ajax:validatorargument/> 标记。验证参数的值可以使用 value 属性规定为静态的,也可以通过使用 sourcefield 属性将其链接到表单字段,将值规定为动态的。

为服务器验证控件构建服务器端验证器

服务器验证器必须实现 IServerValidator 接口,它设置了一个单一方法,即 validatevalidate 方法具有一个参数 arguments,该参数是一个 HashMap,包含针对每个服务器验证参数的名称/值字符串对。IServerValidator 接口如清单 11 所示。

清单 11. IServerValidator 接口
package com.testwebsite.interfaces;

import java.util.HashMap;

public interface IServerValidator {
	/**
	 * The validate method performs the server side validation and returns the 
	 * Server Validator Response (i.e. is valid, message)
	 * @param arguments Server Validator arguments
	 * @return Server Validator Response 
	 */
	IServerValidatorResponse validate(HashMap<String, String> arguments);
}

validate 方法返回 IServerValidatorResponse 对象。在 AutoPopulateServlet 内,IServerValidatorResponse 实例被转换为 JSONObject 并返回给 Web 浏览器以进行客户机端处理。IServerValidatorResponse 接口如清单 12 所示。

清单 12. IServerValidatorResponse 接口
package com.testwebsite.interfaces;

public interface IServerValidatorResponse {

	/**
	 * The getMessage method returns the error/warning message 
	 * displayed if isValid is false. 
	 * @return
	 */
	String getMessage();
	
	/**
	 * The isValid method returns true/false if validated
	 * @return
	 */
	boolean isValid();
}

AccountNameValidatorIServerValidator 接口的一个示例实现,它负责检查 accountName 是否惟一。AccountNameValidator 类如清单 13 所示。

清单 13. 示例验证器 —AccountNameValidator
package com.testwebsite.bll.servervalidators;

import java.util.HashMap;
import com.testwebsite.controls.servervalidator.ServerValidatorResponse;
import com.testwebsite.dal.AccountDataService;
import com.testwebsite.interfaces.IServerValidator;
import com.testwebsite.interfaces.IServerValidatorResponse;

public class AccountNameValidator implements IServerValidator {

	@Override
	public IServerValidatorResponse validate(HashMap<String, String> arguments) {
		ServerValidatorResponse response = null;
		
		if (arguments.containsKey("accountName")) {
			String loginId = arguments.get("accountName");
			AccountDataService actService = new AccountDataService();
			boolean isValid = actService.isAccountUnique(loginId);
			
			StringBuffer message = new StringBuffer();
			if (isValid) {
				message.append("The login id '");
				message.append(loginId);
				message.append("' is valid.");
			}
			else {				
				message.append("The login id '");
				message.append(loginId);
				message.append("' is invalid. An account with 
					the specified name already exists.");
			}
			
			response = new ServerValidatorResponse
				(isValid, message.toString());
		}
		else {
			response = new ServerValidatorResponse
				(false, "The 'Login Id' not specified.");
		}
		
		return response;
	}

}

此接口的另一个示例实现如清单 14 所示。NameValidator 服务器验证器使用两个验证参数来检查 firstNamelastName 参数是否惟一。

清单 14. 示例验证器 —NameValidator
package com.testwebsite.bll.servervalidators;

import java.util.HashMap;

import com.testwebsite.controls.servervalidator.ServerValidatorResponse;
import com.testwebsite.dal.AccountDataService;
import com.testwebsite.interfaces.IServerValidator;
import com.testwebsite.interfaces.IServerValidatorResponse;

public class NameValidator implements IServerValidator {

	@Override
	public IServerValidatorResponse validate(HashMap<String, String> arguments) {
		ServerValidatorResponse response = null;
		
		if (arguments.containsKey("firstName") && 
		arguments.containsKey("lastName") ) {
			String firstName = arguments.get("firstName");
			String lastName = arguments.get("lastName");
			
			if (firstName == null || firstName.length() < 1 
			|| lastName == null || lastName.length() < 1) {
				response = new ServerValidatorResponse(false, "");
				return response;
			}
			
			AccountDataService actService = new AccountDataService();
			boolean isValid = actService.isNameUnique(firstName, lastName);
			
			StringBuffer message = new StringBuffer();
			if (isValid) {
				message.append("The Name '");
				message.append(firstName);
				message.append(" "); 
				message.append(lastName);
				message.append("' is valid.");
			}
			else {				
				message.append("The Name '");
				message.append(firstName);
				message.append(" "); 
				message.append(lastName);
				message.append("' is invalid. An account with 
					the specified name already exists.");
			}
			
			response = new ServerValidatorResponse(isValid, 
				message.toString());
		}
		else {
			response = new ServerValidatorResponse(false, 
				"The 'First Name' and/or 'Last Name' 
				were not specified.");
		}
		
		return response;
	}
}

创建一个 Servlet 来处理异步服务器验证

下一步是构建一个 Servlet 来向 Web 浏览器公开此服务器验证器。Servlet 负责构建传递给 IServerValidator 实现的参数集以及调用 validate 方法。之后它将响应 IServerValidatorResponse 转变成 JSONObject 对象,该对象会被发送回调用 JavaScript 函数进行处理。

Servlet 实现非常类似于在本系列内构建的其他 Servlet。ServerValidatorServlet 类如清单 15 所示。

清单 15. 验证器 Servlet —ServerValidatorServlet
public class ServerValidatorServlet extends HttpServlet {
	private static final long serialVersionUID = -4272040300274058366L;
		
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {		
		this.processRequest(req, resp); }
	
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		this.processRequest(req, resp);	}
	
	/** The processRequest method processes the request (whether get or post) */
	protected void processRequest(HttpServletRequest req, 
		HttpServletResponse resp) throws IOException {		
		String data = "";

		// Get parameters from query string
		String className = req.getParameter("validatorClass");
		String targetField = req.getParameter("targetField");
		String targetFieldValue = req.getParameter("targetFieldValue");		
		Enumeration paramEnum = req.getParameterNames();
		HashMap<String, String> args = new HashMap<String, String>();
		while (paramEnum.hasMoreElements()) {
			String curParam = (String) paramEnum.nextElement();
			if (curParam != null && 
			!curParam.equalsIgnoreCase("validatorClass") &&
					!curParam.equalsIgnoreCase("targetField") &&
					!curParam.equalsIgnoreCase("targetFieldValue")) {
				String curValue = req.getParameter(curParam);
				args.put(curParam, curValue);
			}
		}		
		args.put(targetField, targetFieldValue);
		
		data = this.getJsonResultAsString(className, args);
		resp.setContentType("text/plain");		
		// Write response
		// Get writer for servlet response
		PrintWriter writer = resp.getWriter();
		writer.println(data);
		writer.flush();
	}

	/** Get Result */
	public String getJsonResultAsString(String className, 
		HashMap<String, 
		String> args) {		
		String data = "";		
		// 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[1];
			paramTypes[0] = HashMap.class;			
			Method validateMethod = 
				providerClass.getMethod("validate", paramTypes);
			
			// Construct method param values
            Object[] argList = new Object[1];            
            argList[0] = args;            
            
            // Get instance of the provider class
            Object providerInstance = providerClass.newInstance();
			
            // Invoke method using reflection
            IServerValidatorResponse validatorResponse = 
            (IServerValidatorResponse) validateMethod.invoke(providerInstance, argList);
			
			// Build server JSON response 
			JSONObject jsonResponse = new JSONObject();
			jsonResponse.put("isValid", validatorResponse.isValid());
			jsonResponse.put("message", validatorResponse.getMessage());
			
			// Convert JSONObject result to string
			data = jsonResponse.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();
		} catch (JSONException e) {
			e.printStackTrace();
		}			
	
		return data; }	
}

创建可在 JSP 页面内使用的 JSP TagLib 控件

服务器验证控件可呈现事件处理程序、JavaScript 支持函数以及相应表单字段(即 INPUT)的事件 hook。我们需要如下 JavaScript 函数:

  • 处理 blur 以执行异步服务器调用。
  • 处理异步服务器响应。
  • 获得表单字段值(包含在 <ajax:page/> 之内)。
  • 获得错误消息控件(包含在 <ajax:page/> 之内)。

我们先来看看 onBlur 函数,在这个函数内进行对 ServerValidatorServlet Servlet 的异步服务器调用。该函数由 <ajax:servervalidator/> 控件生成,而且:

  1. 检索被验证的这个控件的值。
  2. 动态构建此目标 URL。
  3. 发送给异步服务器调用进行处理的 post 数据(验证参数)在包含的 <ajax:validatorargument/> 标记基础之上进行动态构建。
  4. 进行异步请求。

<ajax:servervalidator/><ajax:validatorargument/> 标记生成的完整的 onblur 函数如清单 16 所示。

清单 16. 为 onBlur 函数动态生成的代码片段
function accountNameValidator_onBlur() {
	var controlValue = getFormValue('accountName');
	if (controlValue == null || controlValue.length < 1) {
		return;
	}

	var dataUrl = '/ajaxcontrols2/ServerValidator';

	// Post data (dynamically generated by the 
	// <ajax:servervalidator/> and <ajax:validatorarguments/> tags
	var postData = 'validatorClass=
		com.testwebsite.bll.servervalidators.AccountNameValidator&
		targetField=accountName&targetFieldValue=' + controlValue;

	// Initialize XmlHttpRequest object
	initializeXmlHttpRequest();
	
	// If XmlHttpRequest object initialized
	if (req!=null) {
		// Set call back method
		req.onreadystatechange=accountNameValidator_onServerResponse;

		// Update status message
		window.status='Validating data...';

		// Open connection
		req.open('POST',dataUrl,true);
		
		/// Set request header
		req.setRequestHeader("Content-type", 
			"application/x-www-form-urlencoded");
		req.setRequestHeader("Content-length", postData.length);
		
		// Post request
		req.send(postData);
	}
	window.status='';
}

req.onreadystatechange 内指定的回调函数在服务器响应请求的时候调用。如果此服务器验证器返回 false,就会根据 displaymode 属性内指定的模式(javascripthtml)使用 JavaScript 警告或 DHTML 显示一个错误消息。

清单 17 显示了针对 HTML 模式呈现的 onServerResponse 函数。

清单 17. 针对 onServerResponse 函数(HTML 模式)动态生成的代码片段
function accountNameValidator_onServerResponse() {'
	// Get specified control
	var curControl = document.getElementById('accountName');
	if (curControl == null) return;

	// If not complete
	if(req.readyState!=4) return;
	
	// If error
	if(req.status != 200) {
		alert('An error occurred retrieving data.');
		return;
	}

	// Get response data
	var responseData = req.responseText;
	var dataObj =eval('(' + responseData + ')');

	// If response data is null
	if (dataObj == null) { 
		return; 
	}

	// Get corresponding error SPAN control 
	var msgSpan = getErrorMessageControl(curControl);

	if (msgSpan != null) { 
		msgSpan.className='popupvalidator';
	}

	// If valid
	if (dataObj.isValid) {
		if (msgSpan != null) {
			msgSpan.style.visibility  = 'hidden';
		}
	}
	else if (!dataObj.isValid && dataObj.message != '') {
		msgSpan.style.visibility  = 'visible';
		msgSpan.innerHTML = dataObj.message;
		curControl.focus();
	}
}

针对 JavaScript 模式呈现的 onServerResponse 函数如清单 18 所示。

清单 18. 针对 onServerResponse 函数(JavaScript 模式)动态生成的代码片段
function nameValidator2_onServerResponse() {
	// Get specified control
	var curControl = document.getElementById('lastName');	
	if (curControl == null) return;
	
	// If not complete
	if(req.readyState!=4) return;

	// If error
	if(req.status != 200) {
		alert('An error occurred retrieving data.');
		return;
	}

	// Get response data	
	var responseData = req.responseText;
	var dataObj =eval('(' + responseData + ')');
	
	if (dataObj == null) { 
		return; 
	}

	if (dataObj.isValid){
	}
	else if (!dataObj.isValid && dataObj.message != ''){
		alert(dataObj.message);
		curControl.focus();
	}
}

ServerValidator TagLib 库定义项

下一步是在 TagLib 库定义文件内定义 <ajax:servervalidator/> JSP 控件。服务器验证器控件的 TagLib 库定义项(具有针对每个属性描述的内嵌注释)如清单 19 所示。

清单 19. ServerValidator TagLib 库定义项
<tag>
	<name>servervalidator</name>
	<tagclass>com.testwebsite.controls.servervalidator.ServerValidatorTag
	</tagclass>
	<bodycontent>JSP</bodycontent>
	<info>Server validator tag (returns JSON result for 
	asynchronous server validation</info>
	<!-- Unique identifier for control -->
	<attribute>
		<name>id</name>
		<required>true</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>		
	<!-- Message displayed while retrieving values from Value Provider -->
	<attribute>
		<name>updatemessage</name>
		<required>false</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- Target field for validation-->
	<attribute>
		<name>targetfield</name>
		<required>true</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- Fully qualified class name for validator implementation -->
	<attribute>
		<name>validatorclass</name>
		<required>true</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- CSS Class name for popup -->
	<attribute>
		<name>cssclass</name>
		<required>false</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- Clear on invalid-->
	<attribute>
		<name>clearinvalid</name>
		<required>false</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- Refocus if invalid-->
	<attribute>
		<name>refocusinvalid</name>
		<required>false</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute> 
	<!-- Mode of error --> 		
	<attribute>
		<name>displaymode</name>
		<required>false</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
</tag>

ValidatorArgument TagLib 库定义项

下一步是在 TagLib 库定义文件内定义 <ajax:validatorargument/> JSP 控件。验证器参数控件的 TagLib 库定义项(具有针对每个属性描述的内嵌注释)如清单 20 所示。

清单 20. ValidatorArgument TagLib 库定义项
<tag>
	<name>validatorargument</name>
	<tagclass>com.testwebsite.controls.servervalidator.ValidatorArgumentTag
	</tagclass>
	<bodycontent>empty</bodycontent>
	<info>Contains the argument for the server validator tag.</info>
	<!-- Argument name -->
	<attribute>
		<name>name</name>
		<required>true</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- Source field for argument value -->
	<attribute>
		<name>sourcefield</name>
		<required>false</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
	<!-- Value for argument (overrides source field value if specified -->
	<attribute>
		<name>value</name>
		<required>false</required>
		<rtexprvalue>false</rtexprvalue>
	</attribute>
</tag>

构建测试 Web 页面

接下来,我们将构建一些示例页面来测试这些启用了 Ajax 的新控件。使用 Create New Customer 页面测试 <ajax:autopopulate/><ajax:populaterule/> 控件。使用 Register User 页面测试 <ajax:servervalidator/><ajax:validatorargument/> 控件。

Create New Customer 页面

图 7 所示的是测试用的 Create New Customer 页面,它展示了当 disablefields 选项设为 true 的时候,用户看到的这个自动填充控件的外观。

图 7. Create New Customer 页面所展示的自动填充控件(禁用字段模式)
示例页面:测试自动填充控件(禁用字段)
示例页面:测试自动填充控件(禁用字段)

图 8 所示的是测试用的 Create New Customer 页面,它展示了当 disablefields 选项设为 false 的时候,用户看到的这个自动填充控件的外观。

图 8. Create New Customer 页面所展示的自动填充控件(非禁用字段模式)
示例页面:测试自动填充控件
示例页面:测试自动填充控件

此测试页面的 JSP 代码如清单 21 所示。

清单 21. 展示自动填充控件使用的示例页面
<%@ page contentType="text/html; charset=ISO-8859-5" %>
<%@ taglib prefix="ajax" 
uri="/WEB-INF/tlds/ajax_controls.tld"%>
<html>
<head><title>New Customer Information</title>
<link href="core.css" rel="stylesheet" 
type="text/css"/>
<ajax:page/>
</head>

<body>
<div id="container">
<form>
<table class="dialog" cellspacing="0" 
cellpadding="0">
<thead>
<tr><td class="dialogTitle" colspan="2">
Contact 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">Zip Code:						
</td>
<td class="fieldValue">
<ajax:autopopulate id="zipCode" 
providerclass="com.testwebsite.bll.LocationFormDataProvider"
updatemessage="Retrieving data from server...">
<ajax:populaterule fieldname="cityName"
sourceattribute="cityName"/>
<ajax:populaterule fieldname="countyName" 
sourceattribute="countyName"/>
<ajax:populaterule fieldname="stateName" 
sourceattribute="stateName"/>
</ajax:autopopulate>
</td></tr>				

<tr>
<td class="fieldLabel">City:						
</td><td class="fieldValue">
<input type="text" id="cityName" 
size="40"/>
</td></tr>

<tr><td class="fieldLabel">County:						
</td>
<td class="fieldValue">
<input type="text" id="countyName" 
size="40"/>
</td></tr>

<tr><td class="fieldLabel">State:						
</td>
<td class="fieldValue">
<input type="text" id="stateName" 
size="40"/>
</td></tr>
</tbody>
<tfoot align="right" class="buttonPane">
<tr>
<td colspan="2">
<input type="reset" />&nbsp;
<input type="submit" value="Save"/>
</td></tr>
</tfoot>
</table>
</form>
</div>					
</body>
</html>

Zip Code 字段现在是启用了 Ajax 的。当用户用 tab 键移出此字段且 Zip Code 字段失去焦点时,就会向服务器发出异步调用以检索位置数据。而 City、County 和 State 字段都会相应更新。如果 disablefieldstrue,这些目标字段(即 City、County 和 State)是禁用的。

Register User 页面

我们还需要测试 HTMLJavaScript。图 9 所示的是测试 Register User 页面,它展示了在 HTML 模式下,从用户角度看到的服务器验证器控件的外观。

图 9. Register User 页面所展示的 servervalidator 控件(HTML 模式)
示例页面:测试 servervalidator 控件
示例页面:测试 servervalidator 控件

图 10 所示的是测试 Register User 页面,它展示了在 JavaScript 模式下从用户角度看到的服务器验证器控件的外观。它还展示了如何测试这两个字段(First Name 和 Last Name)的惟一性。

图 10. Register User 页面所展示的 servervalidator 控件(JavaScript 模式)
示例页面:测试 servervalidator 控件
示例页面:测试 servervalidator 控件

此测试页面的 JSP 代码如清单 22 所示。

清单 22. 展示服务器验证器控件使用的示例页面
<%@ 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">Register User
</td></tr>
</thead>

<tbody> 
<tr><td class="fieldLabel">Account Name:</td>
<td class="fieldValue">
<input type="text" id="accountName" size="40"/>
<ajax:servervalidator id="accountNameValidator" 
targetfield="accountName"
displaymode="html"
validatorclass="com.testwebsite.bll.servervalidators.AccountNameValidator"
cssclass="popupvalidator">
</ajax:servervalidator>
</td>
</tr>

<tr><td class="fieldLabel">Email Address:</td>
<td class="fieldValue">
<input type="text" id="emailAddress" size="40"/>
</td></tr>

<tr><td class="fieldLabel">First Name:</td>
<td class="fieldValue"> <input type="text"
 id="firstName" size="40"/>
<ajax:servervalidator id="nameValidator" 
targetfield="firstName" displaymode="javascript"
validatorclass="com.testwebsite.bll.servervalidators.NameValidator">
<ajax:validatorargument name="lastName" 
sourcefield="lastName"/>
</ajax:servervalidator>
</td></tr>

<tr><td class="fieldLabel">Last Name:</td>
<td class="fieldValue">
<input type="text" id="lastName" size="40"/>
<ajax:servervalidator id="nameValidator2" 
targetfield="lastName"
displaymode="javascript"
validatorclass="com.testwebsite.bll.servervalidators.NameValidator">
<ajax:validatorargument name="firstName" 
sourcefield="firstName"/>	
</ajax:servervalidator>
</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" />&nbsp;
<input type="submit" value="Save"/>
</td></tr></tfoot></table>
</form>
</div>					
</body></html>

结束语

本文构建在本系列第一篇文章中所介绍的异步通信技术的基础之上。新建的 <ajax:autopopulate/> 控件让您能够基于目标字段自动填充表单字段。新建的 <ajax:servervalidator/> 控件让您能实时执行表单字段数据的服务器端验证,并且无需提交此表单和等待服务器响应。启用了 Ajax 的控件改善了用户体验并使用户界面响应性更好也更直观,这对业务应用程序非常有用。所需代码也不是非常复杂;构建启用了 Ajax 的 JSP 控件只需集成关键块(JavaScript、CSS 和 Java EE 技术)即可。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development, Java technology
ArticleID=375592
ArticleTitle=构建启用了 Ajax 的 JSP TagLib 控件,第 2 部分: 自动填充和字段验证器控件
publish-date=03122009