UML软件工程组织

Cloudscape 与 Ajax 示例
Susan L. Cline (clines@us.ibm.com), Cloudscape 工程师, IBM
 对于 Ajax 应用程序来说,尤其是当客户机和服务器在同一台主机上时,Derby 是很好的数据库,因为 Derby 不需要任何管理,而且可以嵌入到应用程序中。本文介绍了创建一个嵌入式数据库及 Web 应用程序的所有步骤和要求。该应用程序的源代码和可直接运行的文件已打包成 zip 文件,可通过网站下载。在这个应用程序中,Derby 数据库充当数据储存库,而 Jetty Web 服务器或 servlet 容器负责处理 HTTP 请求,Ajax 技术则用于增强客户机的呈现和响应能力。

 简介

Ajax 已快速成为 Web 应用程序开发人员的新宠儿。很多网站,例如 Google Maps 和 Yahoo email,以及其他一些交互式网站,都利用 Ajax,使用户可以通过 Web 浏览器获得丰富的交互体验。

Ajax 不是一种规范,不是一种框架,而且也不是一种 API。它是一组已有的技术,包括 JavaScript、XML、Document Object Model(DOM)、Cascading Style Sheets(CSS)和到 Web 服务器或 servlet 容器的异步 HTTP 请求。Ajax 允许 Web 应用程序在客户机(在这里指的是 Web 浏览器)上对数据进行附加的处理、操纵和格式化。服务器由一个 Web 服务器或 servlet 容器和一个数据存储组成,它可以与客户机在同一个主机上。数据可以放在一个平面文件中,也可以存储在数据库中。在本文讨论的示例应用程序 My Address Book 中,数据存储在一个 Derby 数据库中。

关于 Derby 和 Cloudscape 的名称
 Cloudscape 于 1996 年进入市场,是最初的零管理、可嵌入、100% 纯 Java 关系数据库。在 2004 年 8 月,IBM 将 Cloudscape 10.0 关系数据库产品的副本 Derby 捐献给 Apache Software Foundation(ASF),以帮助促进数据驱动的 Java 应用程序方面的创新。IBM 继续将其 Cloudscape 商业产品作为免费下载的产品,它为核心 Derby 引擎增加了一些特性。目前最新的版本是 Cloudscape 10.1,其中包括 Apache Derby 10.1。

 这个应用程序演示了 Derby 的一些特性:

  • 展望即将在 Cloudscape 10.2 版本中推出的 XML 支持。
  • 使用 Java Database Connectivity(JDBC)Callable 语句创建并调用一个存储过程。
  • 使用 EmbeddedDataSource 来创建连接。
  • 在数据库中将图像文件存储为 BLOB。
 本文讨论如何使用 Cloudscape 和 Ajax 来构建一个简单的 Address Book Web 应用程序。为更好地理解本文,读者应对数据库和 Ajax 技术有基本的了解。

 回页首

 嵌入式应用程序

当谈到应用程序时,“嵌入式(embedded)” 一词指的是为应用程序提供服务或功能的一些组件,终端用户无需负责管理底层子系统,甚至可以不知道底层子系统的存在。应用程序 “只管运行”。对于 “嵌入式” 一词的更通用的定义可以在 Wiktionary 中找到,在那里的定义是:“成为……的一部分,并且被紧密地或安全地围绕着;牢牢地插入到……中。"

示例应用程序将数据库引擎 Derby 和 servlet 容器 Jetty 嵌入在应用程序中。Derby 和 Jetty 都是 100% 纯 Java 实现,它们由应用程序来启动,用户不必启动或停止其他进程或应用程序。

Derby 数据库引擎和 JDBC 驱动程序被包含在一个 jar 文件中,这个文件就是 derby.jar。Jetty Web 服务器也是被打包在一个 jar 文件中,这个文件是 org.mortbay.jetty.jar。然而,为了使用 Jetty 作为 servlet 容器,并满足 Jetty 的登录需求,还需要其他一些 jar 文件。

下一小节解释该应用程序的架构,并阐明嵌入式解决方案有多适合某些 Web 应用程序用例。

My Address Book —— 架构

My Address Book 应用程序是一个简单的 Web 应用程序,它具有以下功能:

  • 添加个人的联系方式信息。
  • 修改(编辑)一个联系人。
  • 删除一个联系人。
  • 上传一个联系人的照片。

按照姓或名对联系人排序,或者只显示那些在数据库中有照片的联系人。
 这个 Web 应用程序的设计使用了 Model View Controller(MVC)方法。视图由使用 JavaScript 和 CSS 的 HTML 页面提供。控制器由一个 servlet 提供,该 servlet 运行在嵌入式 Jetty Web 服务器中,而 Derby 数据库则充当后端,表示该应用程序的模型层。

这个架构与某些 MVC 应用程序略有不同 —— 它还使用了一个 applet,并且模型和控制器是在同一主机的同一 Java 虚拟机(JVM)中运行的。

图 1 展示了该应用程序的整体设计,下一节将展开来详细阐述。
 图 1. My Address Book 架构


 什么?浏览器中包含一个运行 Web 服务器和数据库的 JVM?

图 1 中,箭头旁边的数字标明应用程序的流程,以及引导 Web 服务器和数据库引擎的顺序。

  1. 浏览器请求位于文件系统上的一个 HTML 页面。
  2. startapplet.html 页面在浏览器中被打开,这导致 applet 在 Firefox 浏览器内的 JVM 中运行。这个 applet 现在可以掌控一个完整的 Java 环境,它启动并配置嵌入式 Jetty Web 服务器。
  3. 按下 Start Cloudscape Ajax Demo 按钮时,一个请求被发送至 Web 服务器,以请求返回下一个页面 firstpage.html。
  4. firstpage.html 通过 HTTP 响应发送到浏览器。
  5. 用户在 firstpage.html 的表单中提交用户名和密码时,一个请求被发送回 Jetty 进行处理。
  6. Jetty 创建应用程序类的一个实例 DerbyDatabase,该实例启动嵌入式 Derby 引擎,创建数据库(只此一次),然后创建两个表并装载一些行。
  7. 数据库从 CONTACTS 表中选择一行,并将其值传回到 Web 服务器。
  8. Web 服务器以 HTML 页面(AddressBook.html)的格式将结果发送到客户机,HTML 页面中利用了 JavaScript 和 CSS 技术。

如果此时您回过头去看看看,就很容易理解为什么上一节要那么着重强调 “嵌入式” 这个词。您会发现,Firefox 浏览器中运行着一个 JVM。这个 JVM 允许 applet 启动一个嵌入式 Java Web 服务器,然后再引导一个嵌入式 Java 数据库引擎 Derby,后两者都是在这个 JVM 中运行的。

为什么要这样做?我们会详细回答这个问题,这里先给出一些简单的原因:

  • 嵌入式应用程序比客户机/服务器应用程序更容易管理。
  • 从客户机到服务器的对数据的异步请求需要 HTTP 协议。因此,要做出对数据(在这个例子中,数据存放在数据库中)的异步请求,需要一个 HTTP 服务器。
 回页首

 应用程序的组件

示例应用程序使用了以下软件组件和技术:

  • Derby 数据库引擎和 JDBC 驱动程序
    “服务器” 端的数据储存库
  • Jetty Web 服务器和 servlet 容器
    提供 HTML 页面,并通过一个 servlet 充当数据库请求的控制器
  • Firefox 浏览器和 Java 运行时环境(JRE)
    Mozilla Firefox 1.5.x 使用 Java 1.5.x JRE 来启动 JVM
  • JavaScript
    用于向 Web 服务器作出异步请求,以通过一个控制器 servlet 获取数据。通过 DOMParser 操纵返回的数据。还用于排序客户机上已有的数据。
  • CSS
    对 HTML 页面的内容进行格式化

软件需求

本节中描述的软件可以免费下载,在运行和安装示例 Web 应用程序之前,必须安装这些软件。

  • Mozilla Firefox 1.5.0.x 或更高版本,请参考 参考资料小节。
  • Sun JRE V1.5.0.x 或更高版本,请参考 参考资料 小节。
  • Windows 操作系统(本文给出的应用程序不兼容 Linux)。

用 Firefox 验证 JRE

  • 在下载和安装 Firefox 与 JRE 之后,可以访问 JAVA SOFTWARE for Your Computer,以测试 JRE 在浏览器中的安装。
  • 在撰写本文之际,对于最新版本的 JRE,如果安装正确,那么上述测试的输出如下:
    We detected your Java environment as follows;
    Description Your Environment

    Java Runtime Vendor: Sun Microsystems Inc.
    Java Runtime Version 1.5.0_06

  • 上述验证必须成功,这样才能保证示例应用程序正常运行。如果测试未成功,而您已经下载和安装了正确版本的 JRE,但验证测试中报告说 JRE 的版本不是 1.5.x 或更高的,那么在浏览器中运行 applet 时,可能需要对使用哪个 JRE 进行配置。

    为此,打开控制面板,并启动 Java 图标。选择 Java 选项卡,然后单击 Java Applet Runtime Settings 区域中的 View 按钮。检查一下,确保位置有效。在本例子中,某个 JRE 已经被卸载了,但是仍然有一个无效的位置条目。由于您可能安装了多个 JRE,因此在安装应用程序之前,需要确保浏览器报告使用了正确的 JRE。
  • 在这个例子中,如果 JRE 验证测试报告目前使用的 JRE 版本为 1.5.0_06,那么 Java 控制面板中的 JRE applet 设置如下所示:

图 2. Java 控制面板,applet 设置

示例应用程序 zip 文件:

将 Cloudscape_Ajax_Demo.zip 文件下载到您的文件系统中(请参考 下载 小节)。该 zip 文件中包含了所有的 HTML、JavaScript、CSS 和 Java 源文件,此外,Derby 数据库引擎和 Jetty Web 服务器类文件也包含在其中。

安装应用程序

  1. 安装前面列出的必需软件。
  2. 参考 软件需求 小节中的 “用 Firefox 验证 JRE” 部分,确认在 Firefox 中使用了适当版本的 JRE。
  3. 解压文件 Cloudscape_Ajax_Demo.zip。这样会创建一个名为 Cloudscape_Ajax_Demo 的目录,其中包含以下子目录和文件:

    src
    Java 源文件
    licenses
    应用程序中包括的每个库的许可文件
    cloudscape_ajax_webapp.zip
  4. 将 cloudscape_ajax_webapp.zip 文件解压到 Mozilla Firefox 主目录。由于 Jetty Web 服务器是从这个目录中启动的,因此这个目录被认为是 “主” 目录,HTML 页面将从这个目录提供。 在本例使用的计算机上,Firefox 可执行文件所在目录的完整路径为 C:\projects\Ajax\Mozilla Firefox。解压后,在 Mozilla Firefox 主目录下会有一个 webapps/Ajax 目录。其中包含用于该应用程序的 applet jar 文件 cloudscape_demo.jar(包括所有依赖库)、JavaScript、CSS 和 HTML 文件。

applet jar 文件 cloudscape_demo.jar 包括以下库:

  • derby.jar —— 一个 10.2.x 快照版本的 Derby 数据库引擎和提供 XML 功能的嵌入式 JDBC 驱动程序。不支持 Derby 快照。
  • org.mortbay.jetty.jar —— Jetty HTTP 服务器和 servlet 容器。
  • javax.servlet.jar —— servlet API 类。
  • commons-logging.jar —— Jetty Web 服务器所需的 Logging 类。
  • xercesImpl.jar —— Xerces 解析器,Derby 需要使用它提供 XML 支持。
  • cos.jar —— O'Reilly 库,用于将文件上传到该应用程序所使用的 Web 服务器上(要了解使用该 jar 文件的声明和分发限制,请参考 license 目录。)
  • cloudscape.ajax 包 —— 应用程序类,包括 applet、一个控制器 servlet 以及用于通过 EmbeddedDataSource 访问 Derby 数据库的类,还有一个 XML 辅助类。(所有这些类的源代码均位于主 zip 文件的 src 子目录中。)
 以上 jar 文件中包含的类被提取出来,并存放在一个单独的 cloudscape_demo.jar 文件中,因为初始应用程序类需要访问包含在上述库中的一些 Jetty 和 logging 类。我没有在 applet 标记中列出所有的 jar 文件(这样做需要签发每个 jar 文件),而是从其他 jar 文件中提取出这些类,然后创建一个自签 jar 文件。

回页首

 探索应用程序

在先决条件得到满足,并完成了应用程序的安装之后,启动应用程序,逐步操作并研究幕后所发生的事情。为帮助您理解这里所讨论的每个项目与整个应用程序之间的关系,请参考 图 1 架构图中显示的步骤。

启动 applet 有问题?

请确认 cloudscape_ajax_webapp.zip 被解压到适当的目录。它应该被解压到 Mozilla Firefox 浏览器的 “主” 目录。
再次检查 Firefox 使用的 JRE。即使您下载了正确版本的 JRE,验证测试也可能不显示这个版本的 JRE。请参考 软件需求小节中的 “用 Firefox 验证 JRE” 部分,以便正确设置浏览器中使用的 JRE 版本。

 启动 1.5.0 Firefox 浏览器,打开 Firefox 安装目录下 webapps/Ajax 目录中的 startapplet.html 文件。在这个例子中,该文件的完整路径是 C:\projects\Ajax\Mozilla Firefox\webapps\Ajax\startapplet.html。

如果您已经将 cloudscape_ajax_webapp.zip 文件解压到正确的位置,那么,当打开 startapplet.html 时,首先看到的是如 图 3 所示的 Security 窗口。接受 Susan Cline 用于 StartJetty 应用程序(applet 类)的数字签名。为避免将来打开 startapplet.html 时还出现这个安全警告,可以选中 Always trust content from this publisher 复选框,然后选择 Run。

关于安全警告的注意事项: 应用程序需要读写客户机上的文件(在本例中,客户机和服务器位于同一台计算机上),并且是由一个 applet 启动的。与 applet 相关的安全模型通常不允许这样做。但是通过签发 applet,即可允许可读写文件的访问权限。该应用程序使用一个签发的 jar 文件来实现这一点。

图 3. 用于签发 applet 的 Security 窗口
 

如果在Security 窗口中选择了 Run,那么 Firefox 上的状态条就会显示 Applet StartJetty started。第一个页面是一个 HTML 文件,其中包括启动嵌入式 Jetty Web 服务器的 applet。

applet

startapplet.html 包含 HTML APPLET 标记,用于启动一个 applet。清单 1 中显示的 APPLET 标记还需要包含 applet 的 Java 类的名称以及被列出的 applet 所需的 jar 文件。在 Firefox 浏览器主菜单中选择 View,然后选择 Page Source,以查看这个页面的 HTML 源代码。

清单 2 是 cloudscape.ajax.StartJetty Java 源文件的一个片段,其中显示了 init 方法和 startJetty 方法,所有扩展 Applet 类的 Java 类都必须重写 init 方法。如果您下载并解压了前文所述的 zip 文件,那么可以在 Cloudscape_Ajax_Demo/src/cloudscape/ajax 目录中找到这个文件。

清单 1. startapplet.html 中的 Applet 标记

<APPLET code="cloudscape.ajax.StartJetty.class" codebase="."
width="1" height="1" name="StartJetty"
archive="cloudscape_demo.jar" alt=""></APPLET>

清单 2. 启动 Jetty 的 Applet 类 cloudscape.ajax.StartJetty

public void init() {
System.out.println("StartJetty: init() was called");
setBackground(Color.white);
startJetty();
}

private int startJetty() {
int startStatus = 0;

try {
// Create the server
server = new HttpServer();

// Create a port listener
SocketListener listener = new SocketListener();

listener.setHost("localhost");
listener.setPort(HTTP_SERVER_PORT);
listener.setMinThreads(5);
listener.setMaxThreads(250);
server.addListener(listener);

// Create a context
HttpContext context = new HttpContext();
context.setContextPath("/Ajax/*");
server.addContext(context);

// Create a servlet container
ServletHandler servlets = new ServletHandler();
context.addHandler(servlets);

// Map a servlet onto the container
servlets.addServlet("ControlServlet", "/ControlServlet/*",
"cloudscape.ajax.ControlServlet");

// Serve static content from the context
String home = System.getProperty("jetty.home", ".");
context.setResourceBase(home + "/webapps/Ajax/");
context.addHandler(new ResourceHandler());

// Start the http server
server.start();
startStatus = STARTED_OK;

} catch (java.net.BindException addressUsedExcept) {
System.out.println("Jetty server has already been started on port"+HTTP_SERVER_PORT);
startStatus = STARTED_ALREADY;
} catch (org.mortbay.util.MultiException multiServerExcept) {
System.out.println("Jetty server has already been started on port"+HTTP_SERVER_PORT);
startStatus = STARTED_ALREADY;
} catch (Exception e) {
e.printStackTrace();
startStatus = NOT_STARTED;
}
return startStatus;
}

startJetty 方法设置 HTTP 服务器的主机和端口,接着设置一个 servlet 处理程序,然后为应用程序创建上下文,再为应用程序注册 servlet,最后启动 Web 服务器。当该 applet 被启动时,会调用它的 init 方法,后者又调用 startJetty 方法。

这就是 图 1 中的步骤 1 和步骤 2。

在 startapplet.html 页面中,单击 Start Cloudscape Ajax Demo 按钮。这样将从 Jetty 获取下一个页面 firstpage.html(图 1 中的步骤 3 和步骤 4),该页面提示输入用户名和口令。

此时,您可以用用户名 cloudscape 和口令 ajax 进行登录,也可以创建一个新用户。

如果在 firstpage.html 页面上单击 Login to Address Book 或 Create New User 按钮,则会发生以下处理过程(图 1 中的步骤 5 到步骤 8):

  • 将用户名和口令发送到位于 Jetty Web 服务器的一个 servlet。这个 servlet 创建应用程序类的一个实例,该实例启动 Derby 数据库引擎,并创建一个 Derby 数据库。创建了两个表 —— 一个是 USERS 表,另外一个是 CONTACTS 表。向这两个表中插入同一个示例行。随后对应用程序的调用将启动 Derby 引擎,但是不会重新创建这个数据库。
  • 接下来,如果用户名存在,将对其进行身份验证;如果是一个新用户名,那么将向 USERS 表中插入一行。接下来出现的页面是应用程序的主页面 AddressBook.html。
    servlet

看看 servlet 类 ControlServlet.java 的 init 方法,这个 servlet 类也包含在所下载的 zip 文件中。

当 servlet 容器第一次装载一个 servlet 时,将调用 init 方法。该方法创建一个应用程序类 DerbyDatabase,然后调用它的 initialize 方法。如前所述,这个方法引导数据库引擎,如果还没有创建数据库和表,那么它还会创建数据库和表。

清单 3. 控制器 servlet 的 init() 方法,cloudscape.ajax.ControlServlet

public void init() throws ServletException {
derbyDB = new DerbyDatabase();
derbyDB.initialize();
}

DerbyDatabase 类 initialize 方法的部分清单显示了如何创建一个 EmbeddedDataSource、使用 setCreateDatabase 方法创建数据库、指定数据库名称、创建到数据库的一个连接。

清单 4. 创建 Derby EmbeddedDataSource,cloudscape.ajax.DerbyDatabase

public boolean initialize() {
Connection conn = null;
boolean success = true;
if (isInitialized) {
return success;
}

if (ds == null) {
ds = new EmbeddedDataSource();
ds.setCreateDatabase("create");
ds.setDatabaseName("DerbyContacts");
Statement stmt = null;
try {
conn = ds.getConnection();
} catch (SQLWarning e) {
...

现在我们到了哪一步?您已经登录到应用程序中,并验证了您的用户名和口令,或者创建了一个新用户名和口令并将其插入到 Derby 数据库中。这些努力得到成功后,将出现应用程序的主页面 AddressBook.html,如下图所示。

图 4. 应用程序的主页面,AddressBook.html
 

在这个页面中可以执行各种不同的任务,例如添加、编辑或删除一个联系人,或者对联系人列表进行排序。首先,添加一个联系人。下面的图显示了在单击 Add 按钮并填写好文本字段以添加一个新联系人之后,该页面显示的内容。

图 5. 添加联系人,AddContact.html

现在单击 Add Contact 按钮。如果所有字段都已填好,那么会弹出一个 JavaScript 对话框,询问是否要上传联系人的照片。如果这些字段中有任何一个字段未填写内容,那么会有一个 JavaScript 函数用于发出警告,并提示您应在提交页面之前填好所有字段。单击 Add Contact 按钮时,后台进行了很多的处理工作。

 JavaScript

单击 Add Contact 按钮时,该按钮通过指定要调用的 insertContact 函数,利用 JavaScript 的事件处理功能。该函数做的第一件事情是确保表单中的所有字段都已经填写好了。如果这些字段都填写好了,则创建一个变量,并使该变量包含表单中包括的所有值。接着调用另一个 JavaScript 函数 makeDerbyRequest,该函数以上述变量作为参数。makeDerbyRequest 函数实际上是一个控制点,用于向运行在 Jetty Web 服务器中的 servlet 发出一个异步请求。

清单 5 显示了用于 Add Contact 按钮的 html 代码,清单 6 显示了 JavaScript 中的 insertContact 和 makeDerbyRequest 函数的部分代码。

 清单 5. AddContact.html 中的 JavaScript onclick 事件处理程序

<INPUT type="button" name="add_button" value="Add Contact"
onclick="insertContact()" class="buttons">

清单 6. 处理 Add Contact 按钮的单击事件,address_book.js

var request = false;

try {
request = new XMLHttpRequest();
} catch (trymicrosoft) {
try {
request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (othermicrosoft) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (failed) {
request = false;
}
}
}

function insertContact() {
var isFormValid = validateForm("add_contact_form");
if (!isFormValid) {
showErrors("add_contact_form");
}
else {
var parameters = "&firstname=" + document.add_contact_form.firstname.value ...
...
makeDerbyRequest("insert", parameters);
}

return isFormValid;
}

function makeDerbyRequest(databaseAction, parameters, selectedOption) {
var url = "/Ajax/ControlServlet/?querytype=";
if (databaseAction == "show") {
url = url + "show";
request.open("GET", url, true);
request.onreadystatechange = updateContactList;
request.send(null);
}
else if (databaseAction == "login") {
url= url + "login" + parameters;
request.open("GET", url, true);
request.onreadystatechange = verifyLogin;
request.send(null);
}
else if (databaseAction=="insert") {
url = url + "insert"+ parameters;
request.open("GET", url, true);
request.onreadystatechange = showInsertResults;
request.send(null);
}
...
...

这段代码调用了 makeDerbyRequest 函数,该函数带有一个 databaseAction 参数,其值为 insert,另外该函数还带有一系列的名值对,用于表示联系人的所有属性,例如姓和名。在 makeDerbyRequest 函数中,request 被初始化,并表示一个 XMLHttpRequest 对象。XMLHttpRequest 对象是 Ajax 中使用的 Web 浏览器 DOM 的一个扩展,用于从 HTTP 服务器异步接收 XML 或文本。后面会解释 XMLHttpRequest 对象的 onreadystatechange 属性的回调方法 showInsertResults。

servlet 请求和数据库访问

我们来看看,如果将 XMLHttpRequest 对象随请求发送到 URL 为 http://localhost:8095/Ajax/ControlServlet/?querytype=insert 的 Jetty Web 服务器上,同时发送添加新联系人时填写的表单值相对应的名值对,此时服务器端会发生什么事情。

下面显示的 ControlServlet 类的 doGet 方法首先解析传入的参数,如果 querytype 参数被设为 insert 或 update,则创建一个辅助类 ContactXMLBean,这个类实质上是包含联系人信息的一个 JavaBean。最后,ContactXMLBean 对象被传递给 DerbyDatabase 对象的 insertContact 方法。

清单 7. ControlServlet 类的 doGet 方法

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

String queryType = "";
String user = "";
String password = "";
String newuser = "";
queryType = request.getParameter("querytype");
...

String returnXML = "";

...

if (queryType != null) {
if (queryType.equals("show")) {
returnXML = derbyDB.getContacts();
}
else if (queryType.equals("insert") || queryType.equals("update")) {
String firstname = request.getParameter("firstname");
String lastname = request.getParameter("lastname");
String email = request.getParameter("email");
String phone1 = request.getParameter("phone1");
String address = request.getParameter("address");
String city = request.getParameter("city");
String state = request.getParameter("state");
String zip_code = request.getParameter("zip_code");
String phone2 = request.getParameter("phone2");
String bday = request.getParameter("bday");

ContactXMLBean myContact = new ContactXMLBean(address, bday, city, \
email, firstname, lastname, phone1, phone2, state, zip_code);

if (queryType.equals("insert")) {
returnXML = derbyDB.insertContact(myContact);
}
...

response.setContentType("text/plain");
PrintWriter out = response.getWriter();
out.write(returnXML);
out.flush();
out.close();
}

在研究 DerbyDatabase insertContact 方法之前,需要看看 CONTACTS 表的 CREATE TABLE Data Definition Language(DDL)语句,以及在 DerbyDatabase 类的 initialize 方法中第一次启动 Derby 引擎时创建的存储过程。

CONTACTS 表使用一个 XML 数据类型,当前版本的 Cloudscape 不支持这种数据类型,但 10.2 版本将提供支持。本演示中使用的操作包括读取 XML 数据并对其进行检索。

注意:Cloudscape 和 Derby 的 10.2 官方版目前尚未发布。但现在可从 Apache Derby 网站下载一个 alpha 快照版。本演示中包含的 Derby 数据库类是 10.2 alpha 版,不应将其用于生产环境。

要使用 Derby 或 Cloudscape 来构建您自己的应用程序,可以从 IBM Cloudscape 或 Apache Derby 网站下载当前的 10.1 发布版,具体信息请参考 参考资料 小节。

清单 8. DerbyDatabase.java 中使用了 XML 数据类型的 Create Table 语句

CREATE TABLE APP.CONTACTS(ID INT CONSTRAINT CONTACT_PK PRIMARY KEY, XML_COL XML)

 使用 Derby 中的存储过程时涉及到三个步骤:创建一个 public 类,其中至少包含一个静态方法,接着执行 SQL 来注册这个存储过程,然后再调用该存储过程。DerbyProcedures 类满足第一个需求。对于这个方法,需要注意默认连接的使用,还要将自动提交模式设为 false。在默认情况下,自动提交模式被设为 true。这个过程获得 CONTACTS 表中 ID 列当前的最大值。然后将该值加 1,并将其插入到新行的两个地方 —— 一个地方是 ID 列,另外它还作为 XML 列 XML_COL 的一部分。 稍后,对 CONTACTS 表执行一次选择操作、将内容发送到客户机并通过 JavaScript 进行解析时,如果查看返回到客户机的内容,您就会明白为什么要将 ID 存储在 XML 列中。

XMLPARSE SQL 函数是 Cloudscape 10.2 版中即将引入的一种新的 XML 操作符,用于将 XML 插入到一个列中。

清单 9. cloudscape.ajax.DerbyProcedures 中用于存储过程的静态方法

public class DerbyProcedures {
public static void insertContact(int[] rowNum, String myXMLContact)
throws SQLException {
Connection conn = DriverManager.getConnection("jdbc:default:connection");
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
int currentValue = 0;

ResultSet rs = stmt.executeQuery("select max(id) from APP.CONTACTS");

while (rs.next()) {
currentValue = rs.getInt(1);
}
rs.close();

currentValue++;
stmt.close();
String sql = "insert into APP.CONTACTS (id, xml_col) values ("
+ currentValue + ", xmlparse(document '<contact><id>"
+ currentValue + "</id>" + myXMLContact + "</contact>' preserve whitespace))";
stmt = conn.createStatement();
int numRows = stmt.executeUpdate(sql);
if (numRows > 0) {
rowNum[0] = currentValue;
} else {
rowNum[0] = 0;
}
stmt.close();
conn.commit();
conn.close();
}
}

创建存储过程的下一步是执行 CREATE PROCEDURE DDL,在数据库中注册该存储过程。在 Derby 中, SQL 函数与存储过程之间的差别在于,SQL 函数不能修改数据。而且,存储过程支持 IN、 OUT 和 INOUT 参数。这个存储过程具有一个 IN 参数,该参数是一个以 Java 字符串表示的新的联系人。OUT 参数返回刚刚插入到数据库的行号。

清单 10. 用于将存储过程注册到数据库的 SQL,DerbyDatabase.java

create procedure InsertXMLContact(OUT rowNum integer, IN contact VARCHAR(500))
parameter style java language java external name
'cloudscape.ajax.DerbyProcedures.insertContact'

现在就可以调用存储过程了。DerbyDatabase 类中的这个方法使用 JDBC CallableStatement 类和 CALL SQL 语法来执行这一步。该方法返回的整数与插入到表中 id 列的值相对应。

清单 11. 通过 JDBC 调用存储过程,DerbyDatabase.java

public String insertContact(ContactXMLBean myContact) {
// the id of the row just inserted
int idNum = 0;

try {
Connection conn = ds.getConnection();
CallableStatement cstmt = conn.prepareCall("call InsertXMLContact(?,?)");
cstmt.setString(2, myContact.toXMLString());
cstmt.registerOutParameter(1, Types.INTEGER);
cstmt.execute();
idNum = cstmt.getInt(1);
cstmt.close();
conn.close();
}
catch (SQLException sqlExcept) {
sqlExcept.printStackTrace();
}
if (idNum > 0) {
return String.valueOf(idNum);
} else {
return String.valueOf(0);
}
}

按下 Add Contact 按钮后的发生的最后一个步骤就是处理 servlet 发送回客户机的结果。我展示了从按下 Web 页面上的 Add Contact 按钮后发生的很多事情,接下来我将对上述内容加以总结。

JavaScript onclick() 事件处理程序调用 JavaScript 函数 insertContact()。在进行进一步的处理之前,函数验证是否所有字段都包含值。如果这些字段都包含有值,则调用 JavaScript 函数 makeDerbyRequest。此函数将一个请求发送给 Jetty Web 服务器上的 servlet,其中包含表单中的字段。然后,用 XMLHttpRequest 对象的 onreadystatechange 属性注册 showInsertResults 回调方法。

在服务器端,servlet 解析参数,并将一个请求发送到 Derby 数据库,以便将联系人插入到表中。这一行被插入到使用了 xml 数据类型的一个表中,然后使用 JDBC Callable 接口调用一个存储过程。将联系人插入表以后,数据库返回刚插入的行的 id 列的值。

现在我们可以看看客户机上的回调函数 showInsertResults。

清单 12. 在客户机上处理插入的结果,address_book.js

function showInsertResults() {
if (request.readyState == 4) {
if (request.status == 200 ) {
var response = request.responseText;
if (response != "0") {
confirmAddPhoto(response);
}
else {
alert("There was a problem inserting the contact.");
}
}
else if (request.status == 404) {
alert("Request URL does not exist");
}
else {
alert("Error inserting data, please try again.");
}
}
}

这个函数展示了一种相当标准的处理异步请求的方法。检查 readyState 和 status 的值,只有当响应完成并且状态为 “OK” 或 200 时,才输出 Web 服务器返回的值。

好了,我们已经将一个联系人添加到 Address Book 应用程序中,并且看到了 JavaScript 和 XMLHttpRequest 对象在幕后完成的一些工作。现在让我们来上传我的朋友 Martin 的照片,以响应在添加他之后弹出的对话框。

接下来的页面 AddPhoto.html 是一个标准的文件输入域,使您能够浏览文件系统中的一个文件。在浏览到一个图片文件后,单击 Upload to Cloudscape 按钮。顾名思义,这会把图片上传到 Derby 数据库。

下面显示了 CONTACT_PHOTO 表的 SQL DDL。

清单 13. DerbyDatabase.java 中创建 CONTACT_PHOTO 表的 DDL

CREATE TABLE APP.CONTACT_PHOTO
(id int constraint CONTACT_FK references APP.CONTACTS(id),
fname varchar(30),
lname varchar(30),
filename varchar(30),
picture BLOB(150000),
filesize int)
);

DerbyDatabase 应用程序类中的 insertPhoto 方法使用一个 PreparedStatment,将照片文件作为一个 BLOB 插入到数据库中。为便于阅读,如下代码片段在格式上作了调整,实际的代码不包括 '\' 字符。

清单 14. 使用 setBinaryStream 将一个 BLOB 插入数据库,DerbyDatabase.java

public int insertPhoto(int id, String fname, String lname, String filename,
File photoFile) {
int numRows = 0;
String sql = "insert into APP.CONTACT_PHOTO (id, fname, lname,
filename, picture, filesize) \
values (?,?,?,?,?,?)";

Connection conn;
try {
conn = ds.getConnection();
PreparedStatement prepStmt = conn.prepareStatement(sql);
prepStmt.setInt(1, id);
prepStmt.setString(2, fname);
prepStmt.setString(3, lname);
prepStmt.setString(4, filename);

InputStream fileIn = new FileInputStream(photoFile);
prepStmt.setBinaryStream(5, fileIn, (int) photoFile.length());
prepStmt.setInt(6,(int) photoFile.length());
numRows = prepStmt.executeUpdate();
fileIn.close();
prepStmt.close();
conn.close();
} catch (SQLException sqlExcept) {
SQLExceptionPrint(sqlExcept);
} catch (Exception e) {
e.printStackTrace();
}
return numRows;
}

CONTACT_PHOTO 表的 id 最初被传递到 JavaScript 函数 confirmAddPhoto。该函数再将它传递到下一个页面 AddPhoto.html,该页面最终是由一个 servlet 处理的,后者随后又调用上面显示的 insertPhoto 方法。

回页首

什么?浏览器中包含一个运行 Web 服务器和数据库的 JVM?第 2 部分

在上文中,我们已经列出了在某些用例中适合为 Web 应用程序使用嵌入式架构一些原因。为什么说这样做是令人满意的?这里我们再给出一些理由:

  • 这样可以通过 Web 应用程序存储和访问 Derby 数据库中的信息,而不要求 网络连通性。
  • Web 浏览器对于新用户来说是一种熟悉的用户界面。
  • 移动用户(例如销售人员)可以在没有 Internet 连接的情况下添加和更新联系人信息。
  • 对于此类应用程序的未来扩展来说,当连接可用时,应用程序将与远程服务器同步。

回页首

 探索应用程序 —— 第 2 部分

在 探索应用程序 一节中,我们主要研究了客户机与服务器之间的通信和总体架构。而在第 2 部分中,您将看到按下 Show All 按钮时以及应用程序的主页面被访问时,客户机将如何处理从数据库检索到的结果。此外,本节还将简要介绍按姓或名排序联系人列表的 JavaScript。

为了解从数据库返回联系人时会发生什么事情,您需要回过头来,简单看看它们是如何插入数据库中的。每当向 CONTACTS 表插入一行时,在执行插入操作之前,需要将表示联系人的值(名值对,例如姓或名)转换成 Derby XML 数据类型。辅助类 ContactXMLBean 有一个实用方法 toXMLString,该方法可以将 bean 打包在 XML 元素中。 请查看 清单 11,了解插入新联系人时,在为 CallableStatement 设置参数期间调用的 toXMLString 方法

清单 15. 将 JavaBean 打包在元素标记中,ContactXMLBean

public String toXMLString() {
return "<firstname>" + firstname + "</firstname><lastname>"
+ lastname + "</lastname><email>" + email + "</email><phone1>"
+ phone1 + "</phone1><address>" + address + "</address><city>"
+ city + "</city><state>" + state + "</state><zip_code>"
+ zipcode + "</zip_code><phone2>" + phone2 + "</phone2><bday>" + bday + "</bday>";
}

清单 16 显示了服务器端的方法, JDBC 调用数据库以选中表中的所有联系人的方法。

清单 16. 从数据库检索所有联系人,DerbyDatabase.java

public String getContacts() {
StringBuffer sbuf = new StringBuffer(2000);
try {
Connection conn = ds.getConnection("APP", "APP");
Statement stmt = conn.createStatement();
sbuf.append("<Results>");
ResultSet rs = stmt.executeQuery("SELECT XMLSERIALIZE(XML_COL AS \
VARCHAR(500)) from APP.CONTACTS");
while (rs.next()) {
sbuf.append(rs.getString(1));
}
sbuf.append("</Results>");
rs.close();
stmt.close();
conn.close();
} catch (SQLException sqlExcept) {
sqlExcept.printStackTrace();
}
return sbuf.toString();
}

XMLSerialize SQL 函数是 Cloudscape 10.2 发布版即将引入的新函数之一。目前还不支持该函数。注意,XML 被强制转换为 VARCHAR(500)。由于 Cloudscape 10.1 支持 VARCHAR 数据类型,如果您想修改这个应用程序,使之使用当前版本的 Cloudscape,那么只需要定义 APP.CONTACTS 表,使之包含 VARCHAR(500) 类型的 XML_COL 列,继续使用辅助类 ContactXMLBean 的 toXMLString 方法,并这样更改 select 语句:

SELECT XML_COL from APP.CONTACTS;

 如果 APP.CONTACTS 表中只有一行,该 select 语句的输出将如清单 17 所示。

清单 17. 通过 XMLSerialize 从数据库返回的一个联系人


<firstname>Susan</firstname>
<lastname>Cline</lastname>
<email>susanc@mycorp.com</email>
<phone1>510 589-8888</phone1>
<address>300 East 2nd Street</address>
<city>Oakland</city>
<state>California</state>
<zip_code>98654</zip_code>
<phone2>415 703-6345</phone2>
<bday>July 15</bday>

getContacts 方法可以以字符串的形式返回不止一个结果集。看看它是如何围绕整个结果集添加一个 <Results> 根标记的。这个根标记是必需的,这样可以保证 XML 文档格式良好,在解析 XML 时,这也是必要的。 这就引出了下一个话题,在客户机上解析返回的结果(即联系人),以便显示。

解析和显示联系人

每当单击 Show All 按钮或者装载 AddressBook.html 页面时,就会从数据库异步地检索联系人,然后对进行解析,并将其显示在页面上。

为在装载页面时调用一个 JavaScript 函数,需要指定用于 onload 事件的事件处理程序。下面显示的 AddressBook.html 中的 HTML BODY 标记将 OnPageLoad 函数与 onload 事件相关联,每当装载该页面时,就会调用这个函数。

<BODY onLoad="OnPageLoad(queryStringText)" id="thebody">

 OnPageLoad 函数调用 makeDerbyRequest 函数,调用时带一个参数 show。之前您已经看到过 makeDerbyRequest 函数。清单 18 中最有趣的一部分是回调函数 updateContactList。

清单 18. address_book.js 中的 makeDerbyRequest 和 updateContactList 函数

function makeDerbyRequest(databaseAction, parameters, selectedOption) {
var url = "/Ajax/ControlServlet/?querytype="
if (databaseAction == "show") {
url = url + "show";
request.open("GET", url, true);
request.onreadystatechange = updateContactList;
request.send(null);
}
...
function updateContactList() {
if (request.readyState == 4) {
if (request.status == 200 ) {
// the servlet is returning a string, so I need to
// use responseText vs responseXML
var response = request.responseText;
var parser = new DOMParser();
// this turns the doc into an xml doc so it can be parsed.
var doc = parser.parseFromString(response, "text/xml");

// The xml returned from the server is of the format:
//<Results><contact><firstname>Susan</firstname><lastname>Cline</lastname>
//<email>susan@yahoo.net</email>
//<phone1>510 547 -8888</phone1><address>1569 Balboa</address><city>Oaktown</city>
//<state>California</state><zip_code>94618</zip_code><phone2>510 774-6345</phone2>
//<bday>July 15</bday></contact></Results>

var Results = doc.getElementsByTagName("Results")[0];
if(!Results.getElementsByTagName("contact")[0]) {
alert("No Contacts found - please add one.");
return;
}

// remove existing rows from the select list and populate only with the
// new ones retrieved
var theList = document.getElementById("select_contacts");
theList.length = 0;

// a collection of all contact elements wrapped by the <Results> tag
var allContacts = Results.getElementsByTagName("contact");
var contactOutput = "";
var columns;
var option;
var txt;
var optionValueAttribute;
var optionValue;
var optionId = "";

// iterate through the contacts found
for (var j = 0; j < allContacts.length; j++) {
// columns represents all of the children of the contact
columns = allContacts[j].childNodes;
for (var i = 0; i < columns.length; i++) {
if (columns[i].firstChild) {
// extract the unique integer value but don't include it
// as part of the output string
if (i == 0) {
// this is the value generated from Derby as the id value
optionValue = columns[0].firstChild.nodeValue;
}
else {
// concatenate all the rest of the attributes
contactOutput = contactOutput + " " + columns[i].firstChild.nodeValue;
}
}
}
// add a new option element to the select list
option = theList.appendChild(document.createElement('OPTION'));
// assign the unique integer for each contact as the value of the select option
option.value = optionValue;
// create a text node that contains the attributes of the contact
txt = document.createTextNode(contactOutput);
// append the text to the <option>
option.appendChild(txt);
contactOutput = "";
}
}
else if (request.status == 404) {
alert("Request URL does not exist");
}
else {
alert("Error fetching data, please try again.");
}
}
}

对于 udpateContactList 函数,首先要注意的是特定于 Mozilla 的 DOMParser 对象的使用。该对象用于在客户机上解析 XML 或文本,在这个例子中是解析从服务器返回的字符串。parseFromString 函数允许将字符串解析成 DOM 树。

仅用于 Mozilla Firefox
 正如 软件需求 小节中提到的,这个应用程序只在 Firefox 上运行。该 JavaScript 代码不能用于 Internet Explorer 环境和其他环境。

 接下来,该函数使用 getElementsByTagName 解析 DOM 树返回的文档,以查找 Results 标记,然后查找第一个 contact 标记。如果未找到任何 contact 元素,则给出警告并直接返回,而不作任何进一步的处理。

如果找到一个联系人,那么 AddressBook.html 页面上用于显示联系人的 HTML 选择列表将被清除,这是通过将选择列表的长度设置为 0 来实现的。

接下来的一段代码是一个 for 循环,当变量 j 被初始化为 0 时,该循环首先检索第一个联系人的所有 childNodes。然后遍历所有 childNodes,并将第一个节点 id 设置为变量 optionValue。 optionValue 变量被用作每个 HTML 选择列表的惟一标识符,以便将来可以在客户机上操纵每个表示一个联系人的选项,或者为更新数据库作准备。如果在内层 for 循环中处理的节点不是 firstChild,那么该节点的每个值将与 contactOutput 变量中的前一个值链接起来。

跳出内层 for 循环而进入外层 for 循环时,可以看到如何将一个子节点(OPTION 节点)添加到选择列表中,然后用 optionValue 变量中包含的值为这个选项赋值。接下来,contactOutput 变量被赋给一个文本节点,这个文本节点附加在 OPTION 节点后面,然后重新将 contactOutput 变量初始化为一个空字符串,以供在外层 for 循环的下一次遍历中使用。

在对数据库执行选择操作后返回的所有行都是按照这种方法处理的,这样每次按下 Show All 按钮或装载页面时即可动态创建 HTML 选择列表。

在客户机上排序联系人

刚才,您看过了用于检索联系人并将联系人显示在选择列表中的 JavaScript。下面再来看看,如果这些联系人位于客户机上,如何排序以及操纵这些联系人。为了提高趣味性,我们添加更多的联系人。图 6 显示了 My Address Book 中的所有联系人,暂时按添加的顺序排列。这些联系人中只有两三个联系人有相关照片。

图 6. My Address Book 联系人列表
 

为查看按名字排序的联系人列表,单击 Sort by Firstname 按钮。

图 7. 按名字排序的联系人列表


 也可以按姓排序。

图 8. 按姓排序的联系人列表
 

用于按名字或姓排序的 JavaScript 函数是 sortSelect 函数。该函数带有两个参数 —— 第一个参数是要排序的 HTML 选择列表,第二个参数是比较函数。

清单 19. address_book.js 中的 sortSelect 函数

function sortSelect (select, compareFunction) {
var options = new Array (select.options.length);
for (var i = 0; i < options.length; i++) {
options[i] =
new Option (select.options[i].text, select.options[i].value,
select.options[i].defaultSelected, select.options[i].selected);
}
options.sort(compareFunction);
select.options.length = 0;
for (var i = 0; i < options.length; i++) {
select.options[i] = options[i];
}
}

该函数创建一个新的 JavaScript Array 对象,其长度与作为第一个参数传入的选择列表的长度一致。根据每个旧选项创建一个新的 Option 对象,从而将选择列表中的全部选项复制到新 Array 中。接着,该函数调用 Array 的 sort 方法,调用时使用了一个可选的函数作为参数,这个可选函数用作排序 Array 的算法。然后,选择列表被清空,接着被设置为排序后数组中包含的值。用于对数组进行排序的函数是 compareFunction,该函数是作为参数传递给 sort 函数的。

按下 Sort by Lastname 按钮时,对 sortSelect 函数的调用如下所示:

<INPUT type="button" id="lastname_sort_button" name="lastname_sort_button"
value="Sort by Lastname" onclick="sortSelect(document.select_form.contacts,sortLastName)"
class="buttons">

用于 Array 对象的排序函数是 sortLastName,清单 20 显示了该函数。

清单 20. address_book.js 中的 sortLastName 函数

function sortLastName(option1, option2) {
var lastName1 = extractLastName(option1).toUpperCase();
var lastName2 = extractLastName(option2).toUpperCase();

return lastName1 < lastName2 ? -1 :
lastName1 > lastName2 ? 1 : 0;

}

该函数首先解析选项的第二个词(第一个词是名字),将它变为大写形式,然后比较两个字符串,并返回 -1、1 或 0。

回页首

 结束语

本文展示了如何在一个支持 Ajax 的 Web 应用程序中使用嵌入式数据库和 Web 服务器,其客户机和服务器位于同一台主机上。

该应用程序使用 JavaScript 和 XMLHttpRequest 对象,通过一个运行在 Jetty Web 服务器上的 servlet 从一个 Derby 数据库中检索联系人。

将数据库和 Web 服务器嵌入在 Web 应用程序中有很多好处,例如可以提供常见的访问持久数据的用户界面,而且,存在可用连接时,还可以使应用程序与远程服务器同步。

对于更复杂的应用程序,这个应用程序的架构需要加以修改 —— 不过,示例演示了结合使用 Ajax 和 Derby 的方法,可将其作为基础,帮助您更好地了解将 Derby 嵌入 Web 应用程序有多么轻松。

 

版权所有:UML软件工程组织