第五章
DiscRack 应用程序

这一章介绍了DiscRack应用程序,用这个综合性的例子来讲述Enhydra应用程序开发的关键内容

编译并运行 DiscRack




Enhydra中包含了 DiscRack应用程序,它被安装在<enhydra_root>/examples/DiscRack 目录下.

在这一章中,我们用<DiscRack_root>来表示 DiscRack程序的根目录.

为了编译并运行DiscRack,你需要对安装文件作如下修改:


  1. 打开<DiscRack_root>目录中的config.local.mk.inENHYDRA_DIR和JDKDIR变量分别设置成你的Enhydra 和jdk的路径
  2. <DiscRack_root> 目录中的config.local.mk.in文件更名为config.local.mk
  3. 打开�<<DiscRack_root>/discRack目录中的模板配置文件discRack.conf.in,确保所有的数据库管理器配置是正确的. 见附录A中的"配置数据库管理器" 和"数据库配置."
  4. 在<DiscRack_root>目录下输入 make 编译整个应用程序.
    编译应用程序将会产生DiscRack程序所需要的类和包
    注意: DiscRack程序用到的数据库和相应的data层代码与第四章"建立enhydra应用程序"中讲的是一样的,但包的命名除外.缺省情况下载入的数据库为InstantDB .但是你也可以用微软的Access数据库,数据库文件为<DiscRack_root>/discRack/data/discRack.mdb, 但需要对应用程序配置文件中关于数据库管理器的部分做一些更改,具体请参考附录A, "数据库配置," 来得到更多的关于在你的应用程序中使用 Access 的信息.
  5. 为了使用InstantDB 数据库, 将InstantDB的JAR 文件:idb.jar 和jta-spec1_0_1.jar复制到<DiscRack_root>/lib 目录.
    InstantDB 的驱动程序会被 DiscRack的start 脚本自动装载.
  6. 在命令行上输入下面的命令运行DiscRack:
    cd <DiscRack_root>/output
    ./start
  7. 在浏览器中输入http://Localhost:5555来访问应用程序

    浏览器会显示如下图:

    图形5.1 在浏览器中显示DiscRack 的登陆界面

    操作一下这个应用程序,看一下它是怎样工作的:


    1.点击Register 按钮,注册一个用户, 将一些光盘加入到你的仓库中.

    2.看一下你的仓库,并编辑其中的一个盘.


开发应用程序的步骤和初步知识
--------------------------------------------------------------------------------


在讨论DiscRack这个应用程序怎样工作以前,知道一般情况下怎样开发Enhydra应用程序是非常有用的,你可以采取传统的软件开发方式来开发Enhydra应用程序,保证:

1.应用程序能够像预想地那样工作.

2.项目完成必须及时cost-effective

3.应用程序容易维护和升级

深入讨论软件开发方法已经超出了本书的范围,但是明白其基本原理和它怎样应用于DiscRack应用程序是非常有益的.当你开发一个更复杂的现实的应用程序时,你会受益非浅.
下面的步骤遵循 Lutris Technologies' Structured Delivery Process '(SDP), 这是Lutris公司开发学多项目的过程中使用的一个严格的方法. 这个简化的步骤非常适合小型项目的开发. 要想得到更多的关于大型的团队合作的项目的信息, 参见Lutris公司网站上的SDP 信息:http://www.lutris.com.

一个简化的应用程序开发步骤包含下列几步:


1.定义需求

只要可能的话,对应用程序将要完成的功能做一个详细的综述,这个综述本质上定义了应用程序的总体目标.
2.功能说明

描述应用程序怎样解决需求定义中列出的问题


3.设计和storyboard

设计应用程序的presentation, data, 和business 层, 然后创建storyboard.


4.开发测试

编写代码并测试应用程序.


5.部署

将应用程序打包并安装在其操作环境中.

这个简化的方法描述了开发过程的关键方面. 复杂的实际的应用程序要求更综合的步骤:包括 项目进度控制, 成本分析, 文档等等.下几节讲述了这些简短的步骤.
DiscRack 的需求定义
--------------------------------------------------------------------------------


Otter家族需要一个方法来跟踪他们的收藏的cd.每个家庭成员都有其自己的收藏,有时候他们可能有些混淆: Otters 忘记了哪些是他的. 他们认为用一个Enhydra应用程序来管理他们的CD将是一个完美的方法. 经过讨论以后,他们得出了一个需求分析:

DiscRack将让每一个用户通过增加,编辑,删除来管理自己的CD.应用程序将会keep track of 所有的与CD有关的信息, 包括艺术家和题目.
DiscRack 功能说明
--------------------------------------------------------------------------------


简要的说,对DiscRack有如下要求:


保存用户名和密码

用户必须用用户名和密码登陆才能访问他们的CD目录.


允许用户通过输入他们的姓名,用户名,密码来注册.

一旦登陆成功,用户可以看到它的cd目录,并且可以:


向目录中增加cd.

编辑存在的cd.

在确认的提示下,删除他们的cd.


每个cd显示的信息包括:艺术家, 题目, 种类,以及用户是否喜欢这个cd.
设计和演示面板
--------------------------------------------------------------------------------


这最关键的一步主要是技术上的设计:包括数据库的设计和相应的data层,business层,presentation层的代码. 用户界面的设计可以封装到一个storyboard.
演示图板(storyboard)是用户了解程序流程的形象化工具. 它提供了应用程序用户界面的轮廓和应用程序设计能够进行的框架.It provides an outline of the application's user interface, and a framework from which the rest of the application design can proceed.
一个概念上的演示图板就是应用程序的流程图.A conceptual storyboard, which is largely an application flowchart, is sometimes referred to as a site map, in contrast to a mocked-up HTML storyboard. This book refers to both as a storyboard. DiscRack 的演示图板见图形5.2.

你可以看到演示图板中共有5个html页面,显示CD目录的DiscCatalog页面是程序的中心页面,第一页是
Login 页面; 最后一个页面Logout 页面.
DiscRack 在resources目录下有一个可以工作的演示图板 (或叫做应用程序模型) . 它包括一些列的静态HTML 页面来描述应用程序怎样工作. 为了能看到演示图板,在浏览器中打开下面的页面:
<DiscRack_root>/discrack/resources/personMgmt/Login.html


浏览器将显示DiscRack的login 页面.


点击Login 按钮就会转到光盘目录页面.

点击Sign Up 按钮将会显示注册页面.

点击页面其它的连接你会看到演示图板(storyboard)的剩余部分.

图形5.2所示的HTML页面没有后台的程序来激活它,所有的页面都是静态的. 但是这个演示面板可以让你很好的了解应用程序是怎样工作的.

图形5.2 DiscRack 演示面板

开发测试和部署
--------------------------------------------------------------------------------


为了完成应用程序,剩余的几步包括开发,测试和最终的部署.
当你在应用程序根目录下编译时, make 将会创建一个output目录,这个目录中包含了应用程序的配置文件和 start 脚本. 同样它也会创建一个lib目录,其中包含一个.jar 文件,这个文件中包含了应用程序的所有类和其它一些文件 (例如, GIF文件或样式表文件).
为了部署应用程序,你需要拷贝这些文件到需要这个程序运行的服务器上, 由于改变了地方,所以需要对配置文件作适当的修改.当然, 这个服务器上必须安装了Enhydra ,还有其它辅助的库(例如数据库的JDBC 驱动程序库) .
这一章的剩余部分将讲述DiscRack应用程序本身.

DiscRack 综述
--------------------------------------------------------------------------------


基本的DiscRack 应用程序包含 9个包中的23个类. 表格5.1描述了基本的包和类的功能:
表格5.1 DiscRack 应用程序综述

类及包名 描述
discRack
DiscRack 应用程序对象
DiscRackException 一个简单的基本异常类
Presentation 层/包
BasePO 所有表现层对象的抽象基本类
DiscRackSessionData session 数据的容器
ErrorHandler 对应用程序其它地方没被捕获的异常的处理类
DiscRackPresentationException 表象层的异常类
presentation.personMgmt 包含RegisterLogin 类:管理与 PERSON表有关的信息的表现层类
presentation.discMgmt 包含EditDiscCatalog 类:管理与DISC 表有关的信息的表现层类
Business 层/包
DiscRackBusinessException Business 层的异常处理类
business.person 这个包包含两各类:
  • Person类:表示一个人(person)
  • PersonFactory类:只有一个方法,根据用户名返回一个Person 对象
business.disc 这个包包含两各类:
  • Disc类:表示一个光盘
  • DiscFactory类:有两个方法:根据一个ID或物主的名字返回一个Disc 对象.
Data 层
  参见"Loading the schema."
WAP 层/包
  The DiscRack example application for Lutris Enhydra includes presentation templates and code for wireless access. These additional presentation templates and code comprise what is referred to as wireless profiles. Refer to Appendix C, "Using the DiscRack wireless profiles," in the Wireless Application Developer's Guide for additional information about DiscRack's wireless profiles.

resources目录下有6个HTML文件. 这些文件对应于演示面板中的5个页面和一个error页面,当一个异常没被捕获时,这个error页面会显示所发生的错误.
表现(Presentation)层

表现层包括所有应用程序用户界面用到的的HTML, Java, 和JavaScript 文件.
表现层基本类
DiscRack中所有的表现层对象派生于一个基本类: BasePO, 这个类实现了Enhydra中的HttpPresentation接口. 这个接口有一个方法:run(), 这个方法将HTTP请求作为一个参数.
一个表现层基本类将应用程序通用的功能放到一个地方.注意 BasePO 是一个抽象类, 所以其本身不能实例化,只能子类化.同样其中的许多方法也是抽象的, 所以子类应该实现它们.
BasePO 有许多处理DiscRack中关键任务的方法
:

1.检验用户是否登陆及 session 维护

2:事件处理,根据html产生的方法调用子类对象中的方法Event handling and calling the HTML generation methods in the subclass presentation objects

注意: It is important to realize that you are not required to use a base presentation class. An alternative is to use the Enhydra Application object to perform common tasks.
BasePO 中的关键方法是 run(), 它通过方法调用来实现 session维护和事件处理:
public void run(HttpPresentationComms comms) throws Exception {
// Initialize new or get the existing session data
initSessionData(comms);
// Check if the user needs to be logged in for this request.
if(this.loggedInUserRequired()) {
checkForUserLogin();
}
// Handle the incoming event request
handleEvent(comms);
}


每一次客户端浏览器对表现层对象URL发出请求时,它的处理非常简单:


通过调用initSessionData()初始化或者得到session 数据.

当这个表现层对象要求用户必须登陆时(决定于loggedInUserRequired(),这是一个抽象的方法,每一个表现层对象都实现它), 调用checkForUserLogin() 来检测用户是否已经登陆.如果没有,浏览器就会重定向到 login 页面.

调用handleEvent() 处理 HTML页面中产生的当前事件.

下几节将具体介绍这几个方法.

run() 方法有一个参数, comms, 这是一个包含了HTTP请求信息的对象, 其属性有应用程序(application),异常(exception), 请求(request), 回应(response), session, 和session数据. 这六个属性包括了请求中的所有信息.
例如, 你可以通过 getComms().sessionData.get()得到session数据,通过getComms().request.getParameter()查询字符串参数.
Session 数据和登陆信息
Enhydra session 维护的基础知识已经在"Maintaining session state."介绍过了,相对于那个例子中的session 信息处理的方法, DiscRack 将其所有的session 信息放到一个 DiscRackSessionData对象中,并将这个对象放到用户的session中.
DiscRackSessionData 是一个简单的容器类,包含 get 和set 下列成员属性的方法:

1.一个 Person 对象,表示一个用户

2. 一个名为userMessage String对象, 保存出错信息,例如"Please choose a valid disc to edit"
将session数据放到一个对象中有下列一些优点:


1.可以集中控制session 信息.

当多个表现层对象访问同一个session 数据时,这将非常有用.


2.非常安全.

由于Session.getSessionData() 返回一个一般的Object,如果你将session数据分别存储, 你需要将每个数据转换成适当的类型,那样可能会导致运行期错误,非常难以调试.


3.便于session 数据维护.

如果有大量的session数据,你可以周期性清除不必要的数据.例如为了加速访问,你可能将很多光盘的一个数组放到用户的session 里面, 但是你可能不必等用户退出时才清除这些光盘数据. 通过一个session 数据对象, 你可以很容易的实现一个方法来清除session中不必要的数据.

initSessionData() 方法
表现层对象做的第一件事就是调用initSessionData(). 这个方法的主要部分如下:
Object obj = getComms().sessionData.get(DiscRackSessionData.SESSION_KEY);
if(null != obj) {
this.mySessionData = (DiscRackSessionData)obj;
} else {
this.mySessionData = new DiscRackSessionData();
getComms().sessionData.set(DiscRackSessionData.SESSION_KEY, this.mySessionData);
}


这段代码的第一个语句就是通过session 关键字"DiscRackSessionData"得到session 数据对象, 如果session 数据对象存在, 将其转换成DiscRackSessionData对象; 否则, 创建一个新的DiscRackSessionData 对象, 通过set()方法将其存于用户的session里面.
loggedInUserRequired() 方法
BasePO 类有一个抽象的方法:loggedInUserRequired(),它返回一个 boolean 值, 它表示用户访问相关的页面时用户是否需要登陆. 这样,每一个表现层对象需要实现这个方法.
在BasePO的run()方法中,如果这个方法返回true, checkForUserLogin() 方法就会被调用.
checkForUserLogin() 方法
checkForUserLogin() 方法检测用户是否已经正确地登陆. 如果没有的话, 就会在浏览器中重定向到Login 页面:
...
Person user = getUser();
if (null == user) {
...
throw new ClientPageRedirectException(LOGIN_PAGE);
}

...


为了使这段代码更清晰,将 debug信息写入 log channel的几个语句已经被删除了.
调用getUser() 事实上是调用getSessionData().getUser(), 它返回保存在当前session中的一个Person 对象. 如果用户没有登陆, 或session 已经过期, 这个方法返回null, 同时抛出一个ClientPageRedirectException ,这个异常用login页面的url作为其参数.
当浏览器被ClientPageRedirectException重定向时, any parameters from a query string that were available to the original presentation object are lost. 所以如果你想传递一个错误信息, 你必须将其放到session里面or directly into the query string of the redirected URL.

事件处理
--------------------------------------------------------------------------------


你可以为应用程序中的每个任务创建一个独立的表现层对象, 但很多情况下用一个表现层对象处理多个事件更有意义. 例如:


Edit 表现层对象能够响应4个事件--显示增加页面, 显示编辑页面, 将disc加入到数据库, 从数据库中删除disc .

Login 表现层对象处理3个事件--显示页面, 登陆, 退出.

注意: 在这里,事件(event)是指用户正在处理的任务.
设置事件参数(event parameter)
DiscRack 通过事件参数来跟踪它正在处理的事件,参数放在请求的字符串中.通过事件参数来跟踪正在处理的事件. 例如, 下面这URL指定事件(event)为showAddPage:
http://Localhost:8000/discMgmt/Edit.po?event=showAddPage


DiscRack 讲述了好几种设置事件的方法:


1.showAddPage 事件定义在DiscCatalog.html 页面中,通过JavaScript的onClick 事件来处理 Add 一个新光盘的按钮.

这将会调用JavaScript中的 showAddPage()函数, 这个函数将事件加到请求的URL里面:
document.location='Edit.po?event=showAddPage'


这个函数定义在presentation/discMgmt/DiscCatalogScript.html中, 不是DiscCatalog 页面, 具体解释见"Replacing JavaScript."


2.add 事件(将一个disc加入到数据库) 通过一个隐藏的form field定义在Edit.html 页面:
<input type="hidden" name="event" value="add" id="EventValue">


当用户点击Add按钮时 , event=add 和其它用户输入的form数据一起加到递交请求中(submission request) .


3.exit 事件通过DiscCatalog.html页面中第二个form的ACTION属性定义:
"../personMgmt/Exit.html"


编译时, 这个URL, 就像"URL mapping,"中讲的被替换为:
'../personMgmt/Login.po?event=logout'


尽管 DiscRack 没有实现, 但你可以在抛出一个PageRedirectException时设置事件. 你可以用这个异常实现在一个表现层对象中控制其它的表现层对象. 将下面的字符串加到传递给PageRedirectException构造器的url字符串中,这样就可以指定事件:
"?event=someEvent"


handleEvent() 方法
一旦事件(event)被设定好, BasePO中的handleEvent()方法将会将会进行真正的事件处理:
String event = getComms().request.getParameter(EVENT);
String returnHTML = null;

if (event == null || event.length() == 0) {
returnHTML = handleDefault();
} else {
returnHTML = getPageContentForEvent(event);
}
getComms().response.writeHTML(returnHTML);


这个方法得到请求字符串中的事件参数并调用相应的事件处理方法. 如果发现请求中没有事件参数, 就调用handleDefault(), 这是BasePO中的一个抽象的方法,所以BasePO 的所有子类都会实现它. 否则, 调用getPageContentForEvent(), 这个方法返回指定事件和PO的字符内容.
这个方法包含下面三行:
Method method = this.getClass().getMethod(toMethodName(event), null);
String thePage = (String)method.invoke(this, null);
return thePage;


这段代码使用reflection (定义在java.lang.reflect 包中)根据当前事件调用相应的表现层中的方法. Reflection 可以让你在运行时根据方法名调用一个方法.
调用toMethodName() 时返回一个字符串, handleXxx, Xxx 指当前事件(例如, handleShowAddPage 表示处理showAddPage事件). method.invoke() 然后调用这个方法.
Reflection 允许BasePO 调用其子类中的方法,而不必知道方法名. 只要表象层对象代码中有相应的命名转换这个方案就会工作:

对于每一个事件"foo," 表现层对象类中必须有一个 handleFoo() 方法对事件进行处理.

HTML 页面
--------------------------------------------------------------------------------


你会在<discRack_root>/discRack/resources目录中找到DiscRack所需的HTML 页面. 将HTML页面放在那儿而不是 presentation 目录,这是为了将HTML 文件和Java文件更好的分离开来.尽管对于小的应用程序来说这时多余的,但对于由一个图形设计团队和一个编程团队合作开发的大型应用是非常有好处的..
表现层的make文件控制应用程序怎样使用 HTML 文件. 表现层总共有3个make文件--顶层目录有一个,每个子目录也有一个.
为了将HTML文件放在一个单独的目录而与表现层的类分离, make 文件使用了HTML_DIR 标示, 它表示html文件存放的目录. 例如, 在presentation/Makefile文件中, 你会看到:
HTML_DIR = ../resources


在presentation/discMgmt/Makefile文件中:
HTML_DIR = ../../resources/discMgmt


make 规则将会找到将会找到presentation目录中的HTML 文件(例如, discMgmt/DiscCatalogScript.html).
HTML_CLASSES 标示 表明 XMLC创建的类的名字, 就像"Adding a new page to the application."中讲的.
注意presentation/media 目录中包含了一个make文件. This directory mirrors the final package structure for the .jar file. make 文件中的下面一行将.gif文件复制到最终的d .jar 文件中:
JAR_INSTALL = \ ../../resources/media/*.gif

 

维护演示面板(storyboard)
--------------------------------------------------------------------------------


演示面板开始时只是应用程序的一个模型.但是通过简单的几个步骤,你可以在整个开发过程中维护一个可以工作的演示面板.当用一个图形设计团队和一个编程团队合作开发大型应用时这种能力尤为重要. 每个团队都可以独立地做自己的工作而不受其它团队的影响.
当图形设计团队完成了他们的工作以后,你就可以用新的改进的界面取代旧的,模型化的界面, 新界面中包括更好的图形,JavaScript 特效, 样式表(stylesheets)等等. "Replacing the user interface."中已经讲述了这样的一个例子.
为了保持 HTML文件和Java代码的独立, 就像前一节讲的, 在开发过程中你必须遵循下面三步来维护演示面板:


1.定义url之间的映射,例如 Login.html 映射为Login.po

2.删除HTML文件中的多余数据

3.如果需要的话,替换JavaScript

这三步中的每一步在下面的几节中都有详细的描述:
URL 映射
在可以工作的演示面板中, 像任何静态html页面一样, 超文本连接指向其它的html文件. 也就是说,超文本连接中的 URL 以.html结尾.但是在可以工作的应用程序中, 动态页面的连接指向以.po结尾的表现层对象 . 所以, 你需要做一些工作将演示面板中的"正常的" URL转换为.po .
你可以使用XMLC的-urlmapping 选项将一个URL 映射为另一个,用法如下:
-urlmapping oldURL newURL


为了在编译过程中使用这个选项, 你需要创建一个XMLC 选项文件, 然后通过make文件中的XMLC_HTML_OPTS_FILE标示识别出这个选项文件. 例如:
XMLC_HTML_OPTS_FILE = options.xmlc


在presentation/discMgmt/options.xmlc 文件中包含下面几行:
-urlmapping 'Edit.html' 'Edit.po'
-urlmapping 'DiscCatalog.html' 'DiscCatalog.po'
-urlmapping '../personMgmt/Exit.html' '../personMgmt/Login.po?event=logout'


当XMLC 在这个目录中编译文件时, 它将超文本连接中的url和FORM中ACTION属性中的第一个字符串(例如, Edit.html) 替换为第二个字符串(例如, Edit.po) .
删除多余数据
HTML 文件中经常包含"多余的" 数据,这是为了让演示面板中的页面更像它们运行时的外观.你需要在应用程序中删除这些多余的数据.
看一下presentation/discMgmt/options.xmlc 文件. 特别注意下面这一行:
-delete-class discardMe


-delete-class 选项告诉XMLC 删除所有CLASS属性为discardMe的标记 (还有它们的内容) .例如,如果你看一下 resources/discMgmt/DiscCatalog.html文件, 你会看到下面的html代码:
<tr class="discardMe">
<td>Sonny and Cher</td>
<td>Greatest Hits</td>
<td>Boring Music</td>
<td>Not</td>
</tr>


不是我们不喜欢 Sonny 和Cher, 然而,表格行中的CLASS属性表明这行将会被删除.
不像ID那样, 一个页面中CLASS 属性的值可以不唯一. 你可以用同一个值:discardMe来删除所有的多余的数据.
替换JavaScript
除了替换URL外, 你会经常用程序中实际用到的JavaScript来替换掉演示面板中的JavaScript .例如, resources/DiscCatalog.html文件中包含了下面的 脚本:
<SCRIPT id="DummyScript">
<!--
function doDelete()
{
document.EditForm.action='DiscCatalog.html';
if(confirm('Are your sure you want to delete this disc?')) {
document.EditForm.submit();
}
}
function showAddPage()
{
document.location='Edit.html';
}
//-->
</SCRIPT>


这些函数帮助演示面板能够工作. 运行时,应用程序将会使用真正的函数,这个函数定义在presentation/DiscCatalogScript.html文件中. 例如:
...
function showAddPage()
{
document.location='Edit.po?event=showAddPage';
}

...


由于XMLC 将JavaScript看作注释, URL映射选项将不会影响到 JavaScript函数中的这个URL. 所以在运行时, 你必须在DiscCatalog.java中用下面的代码替换它:
DiscCatalogHTML page = new DiscCatalogHTML();
HTMLScriptElement script = new DiscCatalogScriptHTML().getElementRealScript();
XMLCUtil.replaceNode(script, page.getElementDummyScript());


这是用其它文档中的一个节点替换一个节点的例子.具体实现用到了XMLCUtil 类.
注意: 由于这个行为在运行时发生, 可能对性能有很小的一点影响. 如果性能是最关键的,你可能会在应用程序的最终部署版本中替换 JavaScript .
维护演示面板可能看起来是多余的,没必要的工作, but it is worth the effort when your HTML is evolving in parallel with the Java code. As an example of the power of a working storyboard, you can exchange the HTML in DiscRack from the basic HTML to designed HTML.
替换用户界面
一旦图形设计完成,你可以将应用程序的用户界面替换为其最终的版本y. DiscRack 中包含了一个resources_finished 目录,这个目录中包含了最终版本的" HTML 页面, 还有图形和样式表
为了将以前的演示面板的内容替换为最终的,你需要:


1.将resources 目录改名为resources_old.

2.将 resources_finished 目录改名为resources.

编辑<DiscRack_root>/discRack/presentation/media/Makefile文件中的JAR_INSTALL标示,删除其前面的两个注释号 (#) ,在第一行后面加入一个连接符(\) ,如下所示:
JAR_INSTALL = \
../../resources/media/*.gif \
../../resources/media/*.css \
../../resources/media/*.jpg


这就保证了新的.jpeg 图形和样式表文件会放入应用程序的打包文件.jar 中.


在表现层目录<DiscRack_root>/discRack/presentation中输入下面的命令来重新编译:
make clean
make


make clean 命令会删除所有旧的类, make 将从头开始重新编译应用程序.


现在, 重新启动应用程序并访问它.你会看到改进了的用户界面:

图形5.3 显示改进后的登陆页面


填充列表框
--------------------------------------------------------------------------------


DiscCatalog 页面描述了怎样填充一个选择列表框, 这是一个很常见的工作. 首先, 看一下DiscCatalog.html中SELECT标记的HTML代码:
<SELECT id="TitleList" Name="discID">
<OPTION selected VALUE="invalidID">Select One</OPTION>
<OPTION id="templateOption">Van Halen: Van Halen One</OPTION>
<OPTION class="discardMe">Sonny and Cher: Greatest Hits</OPTION>
<OPTION class="discardMe">Sublime: 40 oz. to Freedom</OPTION>
</SELECT>


现在看一下DiscCatalog.java 中填充列表框的代码.
HTMLOptionElement templateOption = page.getElementTemplateOption();
Node discSelect = templateOption.getParentNode();


第一行根据templateOPTION标记得到一个DOM对象. 第二行调用getParentNode() 得到SELECT标记的容器. 由于SELECT 标记有ID 属性, 这一行也可以这么写:
Node discSelect = page.getElementTitleList();


然后接下来的几行代码会填充表格, 下面这一行用来删除模版行.
templateOption.removeChild(templateOption.getFirstChild());


其它包含CLASS="discardMe"的OPTION 标记, XMLC 会在运行时删除它们,就像"删除多余数据"中讲的.
然后, 通过一个 for 循环不断的得到属于当前用户的光盘, 下面的代码真正的填充列表框:
HTMLOptionElement clonedOption = (HTMLOptionElement)
templateOption.cloneNode(true);
clonedOption.setValue( currentDisc.getHandle() );
Node optionTextNode =
clonedOption.getOwnerDocument().createTextNode(currentDisc.getArtist()
+ ": " + currentDisc.getTitle());
clonedOption.appendChild(optionTextNode);
discSelect.appendChild(clonedOption);


第一行代码拷贝(克隆) 模版选项元素为一个类型为HTMLOptionElement的DOM 对象 . 第二行根据getHandle()返回的值设置其值 , 这个值就是光盘的OBJECTID, 一个唯一的标示符.
第三行(非常长)创建一个包含艺术家名和标题的文本节点. 最终, 最后的两行将文本节点添加到选项节点中,再将选项节点添加到选择节点中.
运行时的HTML代码会像下面这样:
<SELECT name='discID' id='TitleList'>
<OPTION value='invalidID' selected>Select One</OPTION>
<OPTION value='1000001'>Funky Urchin: Lovely Spines</OPTION>
<OPTION value='1000021'>The Seagulls: Screaming Fun</OPTION>
</SELECT>


尽管这个例子不十分明显, 且相当短,但你可以对其进行扩展使其能处理更复杂的情形. 例如, you can modify it to set the default selection based on a second query.

填充form
--------------------------------------------------------------------------------


当用户在列表框中选择了一个光盘并单击Edit Disc 按钮时, 浏览器将会显示一个 form. 如图5.4所示, 编辑表单(edit form)被选中的光盘已有的值填充.用户可以改变这些值,并将改后的值提交到数据库.

图形5.4 DiscRack 光盘的编辑表单

下面是Edit.html中表单元素的html代码.为了清晰, TABLE 标记已经被省去了:
<INPUT TYPE="hidden" NAME="discID" VALUE="invalidID" ID="DiscID">
Artist: <input name="artist" id="Artist" >
Title: <input name="title" id="Title" >
Genre: <input name="genre" id="Genre" >
Do you like this disk?
<input TYPE="checkbox" name="like" CHECKED ID="LikeBox">
<INPUT TYPE="submit" VALUE="Save This Disc Info">


在Edit.java中, 事件处理方法handleDefault() 调用带null参数的showEditPage()来将选中光盘的值填充到表单中. 通常, 仅有的请求参数(除了其它事件类型) 就是光盘的ID, 通过下面这个语句得到:
String discID = this.getComms().request.getParameter(DISC_ID);


下面这些语句得到其它的参数, 但是通常它们为空(but see the error-handling case discussed later):
String title = this.getComms().request.getParameter(TITLE_NAME);
String artist = this.getComms().request.getParameter(ARTIST_NAME);
String genre = this.getComms().request.getParameter(GENRE_NAME);


然后, 调用findDiscByID() 根据ID得到 Disc 数据对象:
disc = DiscFactory.findDiscByID(discID);


接下来的一系列语句会检查title, artist, genre, 和isLiked的值, 通常这些值为空. 因此,下面的语句会被执行(the surrounding if statements are not shown for brevity):
page.getElementDiscID().setValue(disc.getHandle());
page.getElementTitle().setValue(disc.getTitle());
page.getElementArtist().setValue(disc.getArtist());
page.getElementGenre().setValue(disc.getGenre());
page.getElementLikeBox().setChecked(disc.isLiked());


这些语句通过xmlc调用来设置表单元素的值,这些值都是从Disc对象中取得.
当用户完成编辑并点击 Save this Disc Info按钮时, handleEdit() 将会处理编辑后的光盘信息.这个方法调用 saveDisc()方法,试图将新的值存入数据库:

如果成功的话, 客户端将会重定向到DiscCatalog 页面.

如果任何一个新值为空, saveDisc() 将会抛出一个异常.
catch 语句会调用以出错信息作为参数的showEditPage() 方法.
注意: ClientPageRedirectException是java.lang.Error的一个子类,所以抛出异常时它不会被catch语句捕获.
try {
saveDisc(disc);
throw new ClientPageRedirectException(DISC_CATALOG_PAGE);
} catch(Exception ex) {
return showEditPage("You must fill out all fields to edit this disc");
}

The result is that when a user tries to edit a disc and delete some of the values, the edit page redisplays, maintaining all the non-null form element values and restoring the previous values to the null-valued form elements. The page also displays the error string.


Business 层

DiscRack的business 层非常简单, 主要包括:

两个包--Disc 和Person

两个factory 类--DiscFactory 和PersonFactory.
一个factory是这样的一个对象:主要作用就是创建其它对象.
Business 对象
business 对象Disc 和Person 主要封装了相应的data层类: DiscDO 和PersonDO, data对象中的每个属性(或数据库表中的列)都有get 和set 方法. 例如, Disc 对象有getArtist() 和setArtist() 方法.
business 层的对象完成所有的与data层的连接.所以, 如果data 层需要改变, presentation 层将不会受影响. 同样, 如果presentation 层改变了, data 层也不会受影响.
DiscFactory 有两个静态方法:


findDiscsForPerson() 返回属于指定Person对象所拥有的Disc对象的一个数组.

findDiscByID() 根据指定的ID返回一个 Disc 对象.

PersonFactory 有一个静态方法: findPerson(). 它根据指定的用户名返回一个 Person 对象. 如果这个方法从数据库找到多个Person, 它会将错误信息写到log channel中并抛出异常.
使用data 对象
--------------------------------------------------------------------------------


为了帮助你明白DiscRack怎样使用 DODS 产生的data 层代码, 看一下PersonFactory中的findPerson() 方法.为了清晰,注释已经被删除了.
public static Person findPerson(String username)
throws DiscRackBusinessException
{
try {
PersonQuery query = new PersonQuery();
query.setQueryLogin(username);
query.requireUniqueInstance();
PersonDO[] foundPerson = query.getDOArray();
if(foundPerson.length != 0) {
return new Person(foundPerson[0]);
} else {
return null;
}
} catch(NonUniqueQueryException ex) {

...首先, 这个方法创建一个PersonQuery 对象. PersonQuery 是一个data层对象,用来构建和执行对person表的查询. 它有许多setQueryxxx()方法指定查询参数(也就是说, 在SELECT语句中的WHERE子句中设定相匹配的值). 例如, 上面的代码用username作为参数调用 setQueryLogin() 来设置与LOGIN列相匹配的值. 接下来, 调用requireUniqueInstance()方法,这个方法表示查询结果只能是一列, 否则将会抛出异常. 然后调用getDOArray(), 这个方法执行一个查询, 返回一个PersonDO对象的数组. 最后, 根据查询返回一个Person 对象; 如果查询没有找到任何行, 这个方法返回null.