本系列的 第 1 部分 “为 Web 应用程序构建 RESTful 服务” 展示了如何用资源处理程序和数据 API 将一个简单的表作为一个 RESTful 资源公开。此外,还构建了一个简单的 Dojo 应用程序来调用这些服务。
本文将进一步增强该示例以展示如何以 RESTful 风格公开更复杂的数据。并将通过构建第二个 Consumer 应用程序来说明如何用 Zero 为资源建模以及如何用 Zero 基于事件的客户机编程模型构建快捷的富客户机。
第 1 部分 中的练习是将一个优惠表作为 RESTful 服务公开。在本文中将扩展这个示例。图 1 显示了此用例图。
图 1. 用例
这里有两个角色:使用者和优惠。要构建的应用程序有两个:
- 提供者优惠(Provider Incentive)应用程序
- 为使提供者能管理其提供的优惠并能找到目标使用者,本文只侧重于为优惠构建 RESTful 服务。
- 使用者优惠(Consumer Incentive)应用程序
- 允许使用者查找优惠。它们将能通过提供者或位置来搜索优惠。
企业中的平台(比如基于 Java™ EE 的服务器)遵从的是以服务器为中心的思路。Web 应用程序都要构建和部署到应用服务器平台上。服务器平台(比如 WebSphere®)能提供 Java EE 规范 所要求的所有服务质量。这些服务的例子有基于队列的消息传递、分布式事务或协议管理。通常,应用服务器会在同一个 Java 虚拟机上运行若干应用程序。架构师一般会围绕与其他应用程序共享软件和数据资源及共享由应用服务器提供的服务(即使在某些情况下,它们不一定会被用到)的开发思路设计应用程序。
即使应用程序部署于独立的应用服务器内,该服务器本身通常也会承载所有可用的服务。应用服务器允许企业级的集成。企业级集成的特征包括跨多种系统的分布式事务、用于关键数据发送的基于队列的消息传递或是各种其他类型的服务。图 2 中突出显示了一个企业级集成的示例。企业中的平台大都围绕管理各种协议和中间件而设计。有时,它们会与服务很多应用程序的企业数据库对话。
图 2. 企业集成
Web 2.0 还会涉及到 HTTP 级别的一类不是十分关键的集成。应用程序通常围绕一组数据设计,这组数据的目的就是为了公布及与其他数据集混合以创建新的应用程序,这些新创建的应用程序可能并非数据提供者预期的。 图 3 展示了一个集成的示例。
图 3. Web 2.0 中的集成
应用程序围绕数据设计并通过 HTTP 以 REST 风格公开。富 Internet 应用程序之后可以通过混合和匹配这些数据来创建新的情景。例如,一个地图应用程序可以在这个地图上以 REST 风格公开其地点。与之完全不同的应用程序,比如一个雇员管理工具,可以使用这个地图应用程序并加入一组雇员信息来创建雇员工作地点的可视地图。这是通过融合两组数据实现的。在企业环境下,这样的一个应用程序,尽管不是很关键,可能也需要不只一个企业过程完成。
选择以服务器为中心还是以应用程序为中心的设计并不完全取决于伸缩性。以应用程序为中心的伸缩需要一个外部实用工具来对应用程序进行伸缩处理,可能会涉及到诸如 WebSphere XD 这样的软件来管理过程和复制状态。以服务器为中心的设计则会涉及到更加综合的伸缩解决方案,其中应用服务器会有内置的伸缩机制。
如果选择以应用程序为中心的设计,开始时规模可以很小,随着使用者的增加可以很容易地进行伸缩,由此,可以实现 Web 2.0 风格设计的价值主张。另一个重要的特点就是 Web 2.0 风格的应用程序可以调用企业应用程序,反之亦然。图 4 显示了一个能用 HTTP 访问企业应用程序的 mashup。可以利用 WebSphere Web 2.0 Feature Pack 这样的技术来以 REST 风格公开企业工件以便它们能参与到 mashup 或 Rich Internet Application (RIA) 中。
图 4. Mashup 访问企业应用程序
这两种方式各有利弊。以服务器为中心的方式的优点是易于管理,而以应用程序为中心的优点是便于开发人员进行编程,但这种解决方案通常需要外部软件来管理。
RESTful 数据
正如您所见,能以 REST 风格公开数据对于能否成为 Web 2.0 mashup 的组成部分而言非常重要。第 1 部分 讨论了 REST 为何是 Project Zero 的关键。REST 是用来公开资源的。而资源是通过 Internet 公开信息的基础,因此可以方便地使用它组成下一类 Web 应用程序。Mashup 能很快地由多个资源组合而成。Project Zero 的优化也是围绕以 REST 风格公开资源的理念。本文的这个示例中,我们要公开由 energy 提供者提供的优惠。图 5 中给出了此数据模型,显示了这个提供者及其与优惠之间的关系。
图 5. 提供者及优惠的数据模型
其目的是让感兴趣的人能够找到优惠。同时提供者还需要维护这些优惠。表 1 展示了如何以 REST 风格公开这些数据(第 1 部分 展示了这种表如何有助于 REST 风格的资源建模)。
表 1. REST 设计
| 资源 | URI | 方法 | 表示 | 描述 |
| 提供者 | /provider/<providerId> | GET | JSON 对象 | 检索提供者记录 |
| 优惠 | /provider/<providerId>/incentive | POST | JSON 对象 | 创建一个新优惠 |
| 优惠 | /provider/<providerId>/incentive | GET | JSON 数组 | 为特定的 providerId 检索优惠列表 |
| 优惠 | /provider/<providerId>/incentive/<incentiveId> | GET | JSON 对象 | 检索个别优惠 |
| 优惠 | /provider/<providerId>/incentive/<incentiveId> | PUT | JSON 对象 | 更新单个优惠 |
| 优惠 | /provider/<providerId>/incentive/<incentiveId> | DELETE | 删除单个优惠 | |
| 优惠 | /incentive?location=<state_name> | GET | JSON 数组 | 在某个特定状态为任意一个提供者返回一个优惠列表 |
与第 1 部分不同,这里的优惠在提供者名称空间下是可访问的。优惠的生命周期取决于提供者,因而也由其提供者来管理。通过 REST 的风格,我们就能够确保此优惠记录存在于提供者内,从而满足需求。然而,有的使用者可能需要跨多个提供者搜索优惠。这时,可以使用 /incentive 名称空间来提供跨多个提供者的优惠列表。
过多的提供者会使 /incentive 名称空间变得很大,所以需要设法对它做一些限制。这个示例仅允许用指定的位置访问 /incentive 名称空间。我们也可以选择使用查询参数方式来限制选择。
一旦定义了服务,就可以开始为它们附加服务质量了。非功能需求的形式和大小各异(要想获得有关非功能需求的实用信息,请参阅 文章 “Why do non-functional requirements matter?”)。借助 REST,我们所提供的是基于 HTTP 的服务,因而非功能需求的应用可被简化。本示例将处理安全性,这意味着要通过确保 URL 将安全性规则应用于 REST 资源。表 2 显示了这个侧重于安全性的 REST 表的示例。
表 2. 确保 REST 资源的安全性
| 资源 | URI | 方法 | 角色 | 是否需要实例级安全性? |
| 提供者 | /provider/<providerId> | GET | 所有 | 否 |
| 优惠 | /provider/<providerId>/incentive | POST | 提供者 | 是:某提供者只能管理其自身的优惠。 |
| 优惠 | /provider/<providerId>/incentive | GET | 所有 | 否 |
| 优惠 | /provider/<providerId>/incentive/<incentiveId> | GET | 所有 | 否 |
| 优惠 | /provider/<providerId>/incentive/<incentiveId> | PUT | 提供者 | 是:某提供者只能管理其自身的优惠。 |
| 优惠 | /provider/<providerId>/incentive/<incentiveId> | DELETE | 提供者 | 是:某提供者只能管理其自身的优惠。 |
| 优惠 | /incentive?location=<state_name> | GET | 所有 | 否 |
| 任何其他资源 | /<Anything Else> | ALL | 均不适合 | 否 |
表 2 显示了这个 URL 资源及可访问此资源的角色。REST 为公开实体而定义了一种模式,考虑到这一点十分重要。通常,一些数据可能会需要基于实例的安全性;只有数据的所有者才能查看这些数据。在本例中,只有拥有优惠的提供者才能创建、更新和删除优惠,仅仅具有提供者角色是不够的。上面的设计表中有一栏标记了数据是否需要受实例保护。
要想运行本文中的示例,需要:
- Eclipse 3.2 或更高的版本。在本例中用的是 Eclipse 3.3。
- Project Zero 针对 Eclipse 的 Java 和 Groovy 插件。
本示例是用 Zero 的 M3 发布版构建的。请确保 已将 Eclipse Update Site 设为 http://www.projectzero.org/update/zero.eclipse.M3 。
本练习不需要 PHP 插件。要想获得关于安装此插件的信息,请参见 Project Zero 下载。
注意:如果已完成了 第 1 部分 的操作,那么在安装 M3 特性和插件前,必须先卸载 M1 插件。请参考 Eclipse 帮助获取删除插件的相关指导。
- 将本文的 下载 文件解压缩到计算机的 C 盘。
- Firefox 浏览器
- 对本系列第 1 部分中所介绍的概念有很好的理解。
其他有用的工具:
我们将从构建提供者应用程序开始。目标是创建前面定义过的 RESTful 服务。Incentive 应用程序构建于 第 1 部分 中介绍的知识的基础上。
创建新的应用程序:
- 打开一个新的工作空间。
图 6.
- 创建一个新项目。
图 7.
- 选择 Project Zero -> Project Zero Application 作为项目类型。
图 8.
- 将应用程序命名为
ProviderIncentiveApp。
图 9.
必须添加所需要的依赖项。如第 1 部分中所示,ivy 技术可用来管理依赖项(更多细节,请参阅第 1 部分或 Project Zero Developer’s Guide)。
- 打开 config 目录下的 ivy.xml 文件。
图 10.
- 在 Dependencies 下,单击 Add。
图 11.
- 添加下面的依赖项:
- zero.data
- zero.data.setup.webtools
- derby
依赖项向导如下所示。
图 12.
- 保存 ivy.xml 文件。若要查看此资源,其内容应如清单 1 所示。
清单 1<!-- Note: dependencies from maven require the maven2 notation. --> <dependencies> <dependency name="zero.core" org="zero" rev="1.0+"/> <dependency name="zero.webtools" org="zero" rev="1.0+"/> <dependency name="zero.data" org="zero" rev="1+"/> <dependency name="zero.data.setup.webtools" org="zero" rev="1+"/> <dependency name="derby" org="org.apache.derby" rev="10+"/> </dependencies>
- 单击 Update Dependencies 图标。此图标将同时在远程存储库和本地存储库中查找所有配置包的最新版本。
图 13.
- 单击 OK 查找更新。
图 14.
- 完成后关闭编辑器。
我们将使用 derby 的嵌入式版本进行应用程序编程。这是一个很好的开发选择。要创建本例所需的表,需要使用 Project Zero 的 M3 驱动中新引入的数据库设置工具。
- 右键单击 ProviderIncentiveApp 项目。
图 15.
- 选择 General->File System。
图 16.
- 假设已将可下载的文件都解压到了 C: 目录下,选择 C:\ProjectZeroArticleSeries/Part2Artifacts/ProviderAppArtifacts。
选择 sql,然后单击 Finish。
图 17.
- 此时的项目目录布局将如下图所示。
图 18.
- 打开 create.sql
文件,将看到用于创建 PROVIDER 和 INCENTIVE 表的 DDL,如清单 2 所示。
清单 2CREATE TABLE PROVIDER ( PROVIDER_ID VARCHAR (50) NOT NULL, NAME VARCHAR(256) NOT NULL, DESCRIPTION VARCHAR(256) NOT NULL, LOCATION VARCHAR(50) NOT NULL, PROVIDER_TYPE VARCHAR(128), CONTACT VARCHAR(256)); ALTER TABLE PROVIDER ADD CONSTRAINT PROVIDER_PK PRIMARY KEY (PROVIDER_ID); CREATE TABLE INCENTIVE ( INCENTIVEID INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY(START WITH 1, INCREMENT BY 1), NAME VARCHAR(256) NOT NULL, DESCRIPTION VARCHAR(256) NOT NULL, PROVIDER_ID VARCHAR (50) NOT NULL, INCENTIVETYPE VARCHAR(128), VALIDFROM TIMESTAMP, VALIDTO TIMESTAMP, WEBSITE VARCHAR(256) ); ALTER TABLE INCENTIVE ADD CONSTRAINT INCENTIVE_PK PRIMARY KEY (INCENTIVEID); ALTER TABLE INCENTIVE ADD CONSTRAINT INC_PPR_FK FOREIGN KEY (PROVIDER_ID) REFERENCES PROVIDER (PROVIDER_ID);
-
这里有一个用来删除表和添加示例数据的文件。接下来,需要运行应用程序。右键单击项目并选择 Run As -> Project Zero Application。
图 19.
- 检查控制台以确保它已启动并正在运行。
图 20.
- 打开浏览器并进入
http://localhost:8080/setup。浏览器应如下图所示。
图 21.
- 键入下面的信息,然后单击 Create Tables。
- Database Name:
PRO_DB - Database Type: Apache Derby (Embedded)
- User Name:
APP
图 22.
此工具将报告它已成功执行了 create.sql 脚本。
- Database Name:
- 单击 Add Sample Data。此工具应该报告它成功执行了 sample.sql 脚本。
图 23.
- 单击控制台中的 Stop 按钮终止应用程序。在开发时以这种方式启动和终止应用程序是可以的。要想在测试和生产环境中启动和终止应用程序,最好使用管理命令行。更多信息,请参阅
Zero Management CLI extensions reference。
图 24.
要为优惠应用程序创建 RESTful 服务:
- 右键单击 app/resources 文件夹并选择 New -> File。
图 25.
- 将这个文件命名为
provider.groovy。
图 26.
- 单击 Yes 给项目添加 groovy 支持。
图 27.
- 确保 provider.groovy 已打开。输入 清单 3 所示的代码(也可从
C:\ProjectZeroArticleSeries\Part2Artifacts\ProviderAppArtifacts\codeSnippet\proSnippet.1.txt 中将它粘贴过来)。
这段代码使用数据 API 执行查询并将结果存 储为 JSON 格式。此模式在本系列的 第 1 部分 中曾介绍过。请参阅 Project Zero Developer’s Guid 中的 数据访问 一节以获取更多有关 API 的信息。
onRetrieve方法用来表示一种处理程序以获取 Provider 的单个实例。此模式在 第 1 部分 中介绍过(或参阅 Project Zero Developer’s Guide 中的 资源处理)。在这个服务之上还有一个注释。Project Zero 以这种方式对 RESTful 资源进行归档。Project Zero 将用它来呈现 RESTful 文档以及用于 REST 服务的测试工具。稍后还会用到这个工具(更多信息,请参阅 RESTful 文档)。本例中,我们对可用的 HTTP 返回代码进行了归档、对格式进行了描述,还给出了一个返回值的示例。
清单 3import zero.data.groovy.Manager; /** * * @success 200 Returns the profile for the provider with the given ID. * @error 404 Not authorized Provider for User * @format application/json * @example * { * "name": "Energy Provider Name", * "description": "Sample Output", * "location": "New Jersey" * "provider_type": "energy" * "contact": "roland@projectzero.org" * } * */ def onRetrieve() { def data = Manager.create('PRO_DB'); def result = null; def id = request.params.providerId[0]; //Note: Next piece of code should be in one line. result = data.queryFirst ("select name,description,location,provider_type,contact from provider where provider_id = $id"); if(result != null) { request.view='JSON' request.json.output = result render() } else { request.status = HttpURLConnection.HTTP_NOT_FOUND request.error.message = "Provider $id not found." request.view = "error" render() } }
- 在使用这个数据库工具时,数据库的配置会在 data.config 文件内生成。应用程序要使用此配置,必须将它包含在主 zero.config 内。打开 zero.config 文件(第 1 部分 讨论了 zero 配置模型。有关 Zero 配置的细节,请参阅 配置)。
图 28.
- 检查 zero.config 文件的第一部分。请特别注意新的简化了的配置语法。
图 29.
- 下图所示的第二个条目显示了所生成的数据配置。这个用于复杂属性的新语法采用了 JSON 格式。
图 30.
- 用菜单条中的启动按钮启动应用程序。
图 31.
- 用浏览器进入
http://localhost:8080/resources/docs。
下图显示了可用的 RESTful资源。选择 Provider。
图 32.
- 资源文档工具呈现了在 groovy 文件中所注释的信息。此 REST 工具也可用来测试服务。
图 33.
- 通过单击格式,如下所示,可以看到这个示例。
图 34.
图 35.
- 通过单击如下所示的 URI ,可测试此 RESTful 服务。
图 36.
- 单击 Send。
图 37.
- 结果将会显示返回的 JSON 对象。
图 38.
这一节我们将创建优惠服务并将它与提供者服务进行关联。
- 在 /app/resources 文件夹中创建一个名为
incentive.groovy的新文件。
图 39.
- 在 /app/resources 文件夹中创建一个名为
incentive.bnd的新文件。
图 40.
- 输入下面两行:
provider/incentive incentive
以上定义了提供者与优惠之间的关系。在本例中,指定了既支持 /provider/<provider_id>/incentive,也支持 /incentive 名称空间。若想只允许嵌套模式,那么只指定 provider/incentive 即可。
图 41.
- 加入下面的
onList方法(或从 C:\ProjectZeroArticleSeries\Part2Artifacts\ProviderAppArtifacts\codeSnippet/proSnippet3.txt 中粘贴)。 请注意我们使用 groovy 快捷方式检查报头,首先查看提供者是否存在。如果存在,就为提供者执行一个查找所有优惠的查询。如果providerId不存在 ,那么这个请求就是针对 /incentives 名称空间的。这里还有一个对位置参数存在与否的检查。如果存在,就会通过查询执行一个 SQL 搜索。反之,则不允许任何其他调用。
清单 4import zero.data.groovy.Manager; /** * * @success 200 Returns incentives by provider or location. * @error 404 Cannot Query all Incentives in the system. /provider/providerId/incentive or /incentive?location=locationValue must be used. * @format application/json * @example * { * "incentiveId" : "Incentive Id" * "name": "Incentive Name", * "description": "Sample Output", * "providerName": "Provider Name" * "providerId": "Provider Id" * "incentive_type": "Incentive Type" * "validfrom": 1189396800000, * "validto": 1189396800000, * "website": "http://www.projectzero.org" * } * */ def onList() { def data = Manager.create('PRO_DB'); def provider = null def location = null; // List by provider flag if(request.params.providerId) provider = request.params.providerId[0]; // List by location flag if(request.params.location) location = request.params.location[0]; def result = null; if(provider != null) { result = data.queryList("select i.incentiveId,i.name,p.name as providerName,i.provider_id,i.incentivetype as incentive_type,i.validFrom,i.validTo,i.website,p.location from provider as p,incentive as i where p.provider_id = $provider and i.provider_id = p.provider_id"); } else if (location != null) { result = data.queryList("select i.incentiveId,i.name,p.name as providerName,i.provider_id,i.incentivetype as incentive_type,i.validFrom,i.validTo,i.website,p.location from provider as p,incentive as i where p.location = $location and i.provider_id = p.provider_id"); } else { request.status = HttpURLConnection.HTTP_NOT_FOUND request.error.message = "Cannot Query all Incentives in the system. /provider/providerId/incentive or /incentive?location=locationValue must be used." request.view = "error" render() return; } if(result != null) { request.view='JSON' request.json.output = result render() } }
- 保存这个文件。
回到浏览器,进入 http://localhost:8080/resources/doc 并选择 Incentive 链接。
图 42.
- 优惠 URI 应该在此 provider 名称空间下。
在 M3 中有一个 bug,在那里,Resource 文档仅显示了一种资源模式。这在下一个 Milestone 将被纠正。
图 43.
- 单击 URI。要获取 Provider ID,输入
austinEnergy。
图 44.
- 应该会得到 Austin Energy 的优惠列表。
图 45.
- 在浏览器中,试着在
http://localhost:8080/resources/incentive 下查找一个优惠。应该会收到一个如下所示的错误信息。
图 46.
- 使用浏览器或类似 Firefox Poster 这样的工具,执行
GET请求 http://localhost:8080/resources/incentive?location=Texas。下图显示了使用 Poster 工具后的结果。
图 47.
- 返回到 incentive.groovy 文件,输入清单 5 中的代码(或是从 C:\ProjectZeroArticleSeries\Part2Artifacts\ProviderAppArtifacts\codeSnippet/proSnippet3.txt 中粘贴)。这段代码包含检索某个优惠和创建、更新及删除某个优惠的调用。这里的代码与 第 1 部分 中的代码十分相似。
清单 5/** * * @success 200 Returns incentive by id. * @error 404 Incentive Id not found. * @format application/json * @example * { * "incentiveId" : "Incentive Id" * "name": "Incentive Name", * "description": "Sample Output", * "providerName": "Provider Name" * "incentive_type": "Incentive Type" * "validfrom": 1189396800000, * "validto": 1189396800000, * "website": "http://www.projectzero.org" * } * */ def onRetrieve() { def data = Manager.create('PRO_DB'); def id = request.params.incentiveId[0]; result = data.queryFirst("select i.incentiveId,i.name,i.description,p.name as providerName,i.incentivetype as incentive_type,i.validFrom,i.validTo,i.website from provider as p,incentive as i where i.incentiveId = $id"); if(result != null) { request.view='JSON' request.json.output = result render() } else { request.status = HttpURLConnection.HTTP_NOT_FOUND request.error.message = "Incentive $id not found." request.view = "error" render() } } /** * * @success 204 Post new Incentive for the provider. * @error 403 Not authorized to insert this record. * @error 404 Cannot Post directly to /incentives, only /provider/<providerId>/incentive * @format application/json * @example * { * "name": "Incentive Name", * "description": "Sample Output", * "incentive_type": "Incentive Type", * "validfrom": 1189396800000, * "validto": 1189396800000, * "website": "http://www.projectzero.org" * } * */ def onCreate() { def provider = request.params.providerId[0]; if(provider != null) { def data = Manager.create('PRO_DB'); def incentive = zero.json.JsonType.fromData(request.input[]).getJson() def user = request.subject['remoteUser']; def validFrom = new java.sql.Timestamp(incentive.validfrom); def validTo = new java.sql.Timestamp(incentive.validto); def key = data.insert("insert into incentive (name,description,provider_id,incentivetype,validfrom,validto,website) values ($incentive.name,$incentive.description,$provider,$incentive.incentivetype, $validFrom,$validTo,$incentive.website)",['incentiveId']); def locationUri = getRequestedUri(false) + '/' + key request.headers.out.Location = locationUri request.status = HttpURLConnection.HTTP_NO_CONTENT } else { request.status = HttpURLConnection.HTTP_NOT_FOUND request.error.message = "Cannot POST directly to /incentive, only /provider/<providerId>/incentive." request.view = "error" render() return; } } /** * * @success 204 Delete Incentive for the provider. * @error 403 Not authorized to insert this record. * @error 404 Cannot delete directly to /incentives, only /provider/<providerId>/incentive * */ def onDelete() { def provider = request.params.providerId[0]; if(provider != null) { def data = Manager.create('PRO_DB'); def id = request.params.incentiveId[0]; def user = request.subject['remoteUser']; data.update("delete from incentive where incentiveId = $id"); request.status = HttpURLConnection.HTTP_NO_CONTENT } else { request.status = HttpURLConnection.HTTP_NOT_FOUND request.error.message = "Cannot DELETE directly to /incentive, only /provider/<providerId>/incentive." request.view = "error" render() return; } } /** * * @success 204 Incentive updated for provider. * @error 403 Not authorized to update this record. * @error 404 Cannot Put directly to /incentive/<incentiveId>, only /provider/<providerId>/incentive/<incentiveId> * @format application/json * @example * @example * { * "name": "Incentive Name", * "description": "Sample Output", * "incentive_type": "Incentive Type", * "validfrom": 1189396800000, * "validto": 1189396800000, * "website": "http://www.projectzero.org" * * } * */ def onUpdate() { def provider = request.params.providerId[0]; if(provider != null) { def data = Manager.create('PRO_DB'); def incentive = zero.json.JsonType.fromData(request.input[]).getJson() def user = request.subject['remoteUser']; def id = request.params.incentiveId[0]; def validFrom = new java.sql.Timestamp(incentive.validfrom); def validTo = new java.sql.Timestamp(incentive.validto); data.update("update incentive set name=$incentive.name,description=$incentive.description,incentivetype = $incentive.incentiveType,validfrom=$validFrom,validto=$validTo,website= $incentive.website where incentiveId = $id "); request.status = HttpURLConnection.HTTP_NO_CONTENT } else { request.status = HttpURLConnection.HTTP_NOT_FOUND request.error.message = "Cannot Put directly to /incentive/<incentiveId>, only /provider/<providerId>/incentive/<incentiveId>." request.view = "error" render() return; } }
作为需求的一部分,还需要确保对优惠的创建、更新和删除操作针对的是此优惠的提供者。我们将从使用内置的 Zero File Registry 开始。Zero File Registry 是一个不错的开发选择。Project Zero 也支持基于 LDAP 的注册表。一个常见的最佳实践是在产品环境中使用 LDAP。对于 Project Zero 而言,在开发时覆盖默认的注册表十分容易。
- 在浏览器中输入
http://localhost:8080/zero/webtools/user,如下所示。
输入以下内容:
- UserName:
austinEnergy - Password:
passw0rd - Groups:
Provider
单击 Add。
图 48.
- UserName:
- 输入另一个用户,如下所示。
- UserName:
centerPoint - Password:
passw0rd - Group:
Provider
图 49.
- UserName:
- 现在 File Registry 中有了两个用户。
图 50.
- 检查 config 目录,将看到如下所示的 zero.users 文件(可能需要刷新 Eclipse Project)。
图 51.
此时,需要添加授权规则并确保 RESTful URI 的安全性。这里需要用一个不存在的组保护不受支持的 URI。此外,还需要对所有者确保 RESTful 数据的安全性,从这可以看出 Project Zero 对基于实例的安全性的支持。
- 在 config 目录中创建一个新文件。
图 52.
- 将文件命名为
security.config。
图 53.
- 输入 清单 6 中的内容(或是从
C:\ProjectZeroArticleSeries\Part2Artifacts\ProviderAppArtifacts\codeSnippet\proSnippet4.txt 中粘贴)。
rule.config 已包括在内,并且输入了一组参数。
uri定义 REST url。垂直滚动条中的内容也将把 URI 模式之后的所有事情考虑进去。第一个规则会阻止任何人在 /incentive 名称空间上执行 update HTTP 方法。第二个规则仅允许已登录的提供者执行 update 方法。例如,如果我以 austinEnergy 登录,那么我只可以在 austinEnergy 名称空间上发出 POST、DELETE 或 PUT。更多信息,请参阅 Project Zero Developer’s Guide 中的 安全性考虑 一节。
清单 6@include "${/config/dependencies/zero.core}/config/security/rule.config" { "uri":"/resources/incentive|", "condition":"urimatches", "authType":"Basic", "groups":"[NO_ONE]", "methods":"DELETE|POST|PUT" } @include "${/config/dependencies/zero.core}/config/security/rule.config" { "uri":"/resources/provider/{remoteUser}|", "condition":"urimatches", "authType":"Basic", "methods":"DELETE|POST|PUT" }
- 打开 config 目录中的 zero.config 文件。
图 54.
- 为 security.config 文件添加一个
include项。
图 55.
至此已确保了 RESTful 资源的安全性,现在就可以用 REST 文档工具测试安全性了。
- 终止(或通过单击控制台上的 Stop 按钮) 并重启 Provider 应用程序。可以使用快捷方式进行启动。
图 56.
- 让浏览器进入到
http://localhost:8080/resources/docs。
单击 Incentive。
图 57.
- 将看到所有的可用 HTTP 方法,如下所示。
图 58.
- 单击 GET 获取单个记录。
图 59.
- 输入
austinEnergy作为 Provider ID;输入1作为 Incentive ID。
图 60.
- 应该可以获得此 JSON 对象。
图 61.
- 关闭结果窗口。
单击 POST 的格式。
图 62.
- 复制此 JSON 示例。
图 63.
- 单击 POST URI。将值粘贴到 Body 并输入
austinEnergy作为 Provider ID。
图 64.
- 此时,应该进行身份验证。
输入
austinEnergy作为用户名;输入passw0rd作为密码。
图 65.
- 得到的结果如下所示。记录提交后结果的 Location。
图 66.
- 如果有兴趣,可以用 5 测试 PUT 和 DELETE。Delete 应如下图所示。
图 67.
- 以 austinEnergy 登录后,重新用 URI 打开 POST。
试着在
centerPointURI 下 POST 数据。
图 68.
- 应该会得到 403 Forbidden 错误。
图 69.
本练习中,我们已经完成了应用程序的 Incentive 部分。在构建 Consumer 示例时,此应用程序还要保持运行。
至此,您应该已经了解了如何使用数据 API 构建资源处理程序。在 M2 中,Project Zero 引入了另一种为资源建模的方法。一些开发人员倾向于对 SQL 进行编程,而另一些开发人员则更倾向于使用一种持久技术。Zero 资源模型更侧重于将数据映射到 REST(而不一定是对象)。
除 Zero 资源外,您还将使用 Zero Web 模板技术来构建一个快捷的客户机,此客户机将利用 Zero Connection API 调用远程的 REST 资源。
第一步是创建一个新应用程序。
- 创建一个名为
ConsumerIncentiveApp的新 Project Zero 应用程序。
图 70.
- 右键单击 ConsumerIncentive 应用程序并选择 Import。
图 71.
- From 目录应为
C:\ProjectZeroArticleSeries\Part2Artifacts\ConsumerAppArtifacts。只选择 sql 文件夹,如下所示。
图 72.
- 查看如下图中所示的 create.sql 文件。
图 73.
- 注意这个很简单的 Consumer 表只有 4 个字段。
图 74.
- 打开 zero.config 文件。
图 75.
- 将默认端口数改为
8081,以便能同时运行两个应用程序。
图 76.
- 打开 ivy.xml。
图 77.
- 添加下面的依赖项:
- derby
- dojo
- zero.data
- zero.data.setup.webtools
- zero.web.template
- zero.resource
图 78.
- 更新依赖项。
图 79.
- 运行 Consumer。
图 80.
- 检查控制台以确保它在端口 8081 上运行。
图 81.
- 为 Consumer 应用程序运行数据库设置工具。Port 应为 http://localhost:8081/setup/。
输入
CON_DB作为数据库名,Apache Derby (Embedded) 作为数据库类型,APP作为用户名。
图 82.
- 单击 Add Sample Data。
图 83.
- 再次为应用程序打开 zero.config 文件。输入清单 7 中的内容(或是从
C:\ProjectZeroArticleSeries\Part2Artifacts\ConsumerAppArtifacts\conSnippet1.txt 中粘贴)。这将告知资源模型该使用哪个数据库。
清单 7config/db/zero-resource/config= { "class":"org.apache.derby.jdbc.EmbeddedDataSource", "databaseName":"CON_DB" }
图 84.
- 终止 Consumer(而非 Incentive)应用程序。
本节,将在 Consumer 表的基础上创建一个简单的资源模型。由于 M2 还只是这项技术的预览发布,所以我们将采用一种非常简单的模式来演示它的用法(在本系列的后续部分,我们将使用一个稍微复杂一些的示例)。
- 右键单击 app 目录
并选择 New -> Source folder 来创建一个新资源文件夹。
图 85.
- 创建一个名为
app/models的文件夹。
图 86.
- 在 app/models 目录中创建一个名为
consumer.groovy的新文件。
图 87.
- 单击 Finish 添加 Groovy 支持。
- 将清单 8 中的代码输入到 consumer.groovy(或是从
C:\ProjectZeroArticleSeries\Part2Artifacts\ConsumerAppArtifacts\conSnippet2.txt 粘贴)。这样就创建了一个简单的模型。
清单 8import zero.resource.fields.* fields = [ name: [type:'CharField'], state:[type:'CharField'] , provider:[type:'CharField'] ]
- 现在就可以用 Zero API 或 HTTP 在本地访问数据了。
要用 HTTP 公开模型,需要在 /app/resources 中创建另一个文件。
图 88.
- 将它也命名为 consumer.groovy。这时会出现一个已存在 consumer.groovy 文件的错误报警,可以忽略此错误。
图 89.
- 输入代码
ZRM.delegate(),如下所示,它将利用 HTTP 公开模型。
图 90.
- 如果 Consumer 应用程序正在运行,就终止 Consumer(而非 Provider)应用程序,然后再启动此 consumer 应用程序。
图 91.
- 打开浏览器(或用 Poster Firefox 插件)进入
http://localhost:8081/resources/consumer。
图 92.
- 打开结果 JSON 文本,查看使用者列表。
图 93.
- 打开以下 URL:
http://localhost:8081/resources/consumer/
1
。将会得到一个记录。
图 94.
- 应该会返回一条单个记录。
图 95.
我们将在这一节用 Zero Web 模板创建一个很简单的富 Internet 客户机。 第 1 部分 曾展示了 Dojo 客户机如何调用 RESTful 服务。Zero 也支持其他模式,比如访存 HTML 片段,我们在后面会对它做进一步说明(在这个系列的后续部分,我们将联合使用 Dojo 和 Zero 的模板功能)。关于 Zero 客户机的更多内容,请参见 Writing rich Web applications。
Zero 客户机编程模型让 Zero 服务器事件能够响应 JavaScript 事件。它们还可以使用全局上下文的客户机区共享数据。
- 在 Public 目录下创建一个新文件。
图 96.
- 将它命名为
incentiveSearch.zhtml。
图 97.
- 输入 清单 9 中的 HTML 代码(或从
C:\ProjectZeroArticleSeries\Part2Artifacts\ConsumerAppArtifacts\conSnippet3.txt 中粘贴)。
这个 HTML 模板使用了两个 Dojo Grid 组件:一个用来保存使用者数据,另一个用来保存优惠结果。使用 groovy 语言让您可以在服务器或是客户机上创建应用程序事件以响应 UI 事件。Project Zero 使用下面的定义语法来声明一个具有 Ajax 生命周期的应用程序事件:
on("<UI_event>").fire("<application_event>").before ("<before_event>").after("<after_event>")
before_event 指定在所有 Ajax 请求之前所触发的事件,用来显示加载、数据验证等。 after_event 指定在所有 Ajax 请求之后所触发的事件,用来隐藏加载、UI 更新等。 application_event 已触发的事件,比如更新/访存数据、更新 UI 或任何定制的 biz/UI 逻辑。
在运行时,事件处理次序为:before_event --> application_event --> after_event。
application_even 应在服务器端处理,这样 Ajax 生命周期才有意义。before_event 和 after_event 通常在客户机端处理。更多信息,请参见 Writing Rich Web Applications。
清单 9<html> <head> <title>Incentive Search</title> <style type="text/css"> @import "incentiveSearch.css"; </style> <script type="text/javascript" src="/dojo/dojo.js" djConfig="isDebug: true, parseOnLoad: true"></script> <script type="text/javascript"> dojo.require("dojox.grid.Grid"); dojo.require("dojox.grid._data.model"); dojo.require("dojo.data.ItemFileWriteStore"); dojo.require("dijit.form.ValidationTextBox"); dojo.require("dijit.form.DateTextBox"); dojo.require("dojox.grid._data.editors"); dojo.require("dojox.grid.editors"); dojo.require("dojox.grid._data.dijitEditors"); dojo.require("dojo.parser"); </script> <% on(".search:submit").fire("incentiveSearch").after("incentiveRender") %> </head> <body class="tundra"> <h1> Consumer Incentive Search </h1> <br> <br> <form class=.search> <div id="consumerGrid" dojoType="dojox.Grid" structure="consumerLayout"> </div> </form> <br> <br> <hr> <div id="incentiveGrid" dojoType="dojox.Grid" structure="incentiveLayout"> </div> </body> </html>
- 在 Public 目录中创建另一个名为
incentiveSearch.groovy的文件。这个文件将包括响应客户机事件的事件处理程序。
图 98.
- 输入 清单 10 中的代码(或从
C:\ProjectZeroArticleSeries\Part2Artifacts\ConsumerAppArtifacts\conSnippet4.txt 中粘贴)。
当模板装入时,
onInitialize方法会被调用。对于资源模型,我们使用本地 API 获得使用者并将它放在客户机区中。更多关于 Resource API 的信息,请参见 Programmatic Model API。对于每个事件,这里都有一个相应的事件处理程序。它检查请求参数是否存在,并据此决定是执行提供者搜索还是位置搜索(更多相关信息,请参见 Using the Connection API)。 我们还使用了 JSON API 将有效负载转入客户机区。这个客户机区可由 JavaScript 客户机和服务器 groovy 代码访问。
清单 10import zero.core.connection.*; import zero.resource.*; def onInitialize() { def collection = TypeCollection.retrieve('consumer'); def result = collection.list(); def items = []; for(resultItem in result) { def item = [name:resultItem.name ,id:resultItem.id ,state:resultItem.state, provider:resultItem.provider ] items.add(item); } client.data.consumer = items; client.data.incentives = []; } def onIncentiveSearch() { if(event.input.location[]) { Connection.Response response = Connection.doGET("http://localhost:8080/resources/incentive?location= ${event.input.location[]}"); client.data.incentives = zero.json.java.JSONArray.parse(response.getResponseBodyAsString()); } else if(event.input.provider[]) { Connection.Response response = Connection.doGET("http://localhost:8080/resources/provider/ ${event.input.provider[]}/incentive"); client.data.incentives = zero.json.java.JSONArray.parse(response.getResponseBodyAsString()); } else { client.data.incentives = []; } }
- 在 Public 目录中创建一个新文件并将它命名为
incentiveSearch.js。此文件将会具有客户机事件。 这些回调被用来呈现服务器调用的结果。
图 99.
- 输入下面清单 11 中的代码。Dojo FileStore 与 Grid API 被用来本地加载数据。参考 Grid 文档以获取更多关于使用 Dojo Grid 的信息。
清单 11formatDate = function(date) { var d = new Date(); d.setTime(date); return d; } formatLocationLink = function(location) { var locationLink = '<input type="submit" name="location" class="button linktd" value="'; locationLink += location; locationLink += '"/>'; return locationLink; } formatProviderLink = function(provider) { var providerLink = "<input type='submit' name='provider' class='button linktd' value='"; providerLink += provider; providerLink += "'/>"; return providerLink; } var consumerLayout = [{ cells: [[ {name: 'Id', field: "id", width:"10%"}, {name: 'Name', field: "name", width:"30%"}, {name: 'State', field: "state", width:"35%",formatter:formatLocationLink}, {name: 'Provider', field: "provider", width:"25%",formatter:formatProviderLink} ] ] }]; var incentiveLayout = [{ cells: [[ {name: 'Provider', field: "providername",width:"20%"}, {name: 'Type', field: "incentive_type",width:"20%"}, {name: 'Provider Location', field: "location",width:"20%"}, {name: 'Valid From', field: "validfrom",width:"20%",formatter:formatDate}, {name: 'Valid To', field: "validto",width:"20%",formatter:formatDate} ] ] }]; var consumerMeta = { id:'id', items:[], label:'consumer' } var incentiveMeta = { id:'incentiveid', items:[], label:'incentive' } function onLoad(){ zfire("renderConsumers"); } function onRenderConsumers() { consumerMeta.items = dojo.clone(zget("/client/data/consumer")); console.debug(consumerMeta.items); var consumerStore = new dojo.data.ItemFileWriteStore({data: consumerMeta}); var consumerModel = new dojox.grid.data.DojoData(); consumerModel.store = consumerStore; consumerModel.query = {id:'*'}; var consumerGrid = dijit.byId('consumerGrid'); consumerGrid.setModel(consumerModel); } function onIncentiveRender() { incentiveMeta.items = dojo.clone(zget("/client/data/incentives")); console.debug(incentiveMeta.items); var incentiveStore = new dojo.data.ItemFileWriteStore({data: incentiveMeta}); var incentiveModel = new dojox.grid.data.DojoData(); incentiveModel.store = incentiveStore; incentiveModel.query = {incentiveid:'*'}; var incentiveGrid = dijit.byId('incentiveGrid'); incentiveGrid.setModel(incentiveModel); }
- incentiveSearch.css 文件用于增强 UI。将
C:\ProjectZeroArticleSeries\Part2Artifacts\ConsumerAppArtifacts\incentiveSearch.css
导入到 public 目录中。
图 100.
- 通过打开浏览器并进入
http://localhost:8081/locationSearch.zhtml 启动 UI。客户机应该按如下所示呈现。
图 101.
- 单击 Texas 链接以触发一个优惠搜索。
图 102.
- 同样地,单击每个 provider 以触发提供者搜索。
图 103.
图 104.
本文介绍了有关 Zero 的一些新概念,讨论了以应用程序为中心的设计,着重介绍了如何公开复杂数据和确保基于 URL 的资源的安全性。此外,您还了解了新的资源建模和 Zero 客户机 API。
本系列的下一期文章将进一步讨论资源建模,重点介绍使用 Dojo 1.0 和 Zero 客户机编程模型来组装更多的解决方案。
| 描述 | 名字 | 大小 | 下载方法 |
|---|---|---|---|
| 本文的示例代码 | Part2Artifacts.zip | 10KB | HTTP |
学习
-
了解有关 Project Zero
简单环境的方方面面以便基于流行的 Web 技术创建、组装和执行应用程序。
- Project Zero
Developer's Guide
给出了定义 Zero 应用程序体系结构的核心概念。
- 使用 Project Zero
论坛 获得对进行中开发的帮助、反馈、提醒和讨论等等。
-
“Why do non-functional requirements matter?”(developerWorks,2006 年 1 月)是有关非功能性要求的实用介绍。
- 了解 Groovy,一种面向 Java 平台的敏捷动态语言。
- 阅读如何
安装和配置 PHP
以开发 Project Zero 应用程序和面向 Project Zero 的新的 PHP 扩展。
- 参阅 developerWorks 上其他的
有关 Project Zero 的文章和教程。
- 参阅 developerWorks 上其他的
有关 REST 的文章和教程。
- 全面了解 Dojo、JavaScript 工具箱和 Dojo 小部件。
- 阅读 Roy Thomas Fielding 撰写的论文 “Architectural Styles and the Design of
Network-based Software Architectures” 中有关
Representational State Transfer (REST)
的内容。
- 获得有关 Project Zero 中的 JSON 和
JSON 支持
的细节。
- 全面了解 Eclipse,一种开放的开发平台。
- 获得有关 Mozilla
Firefox Web 浏览器和
Poster 插件的更多细节。
- 了解 Apache Derby,一种完全在 Java 中实现的开源关系型数据库。
- 通过面向站点作者和站长的
缓存教程 获得更多有关缓存的信息。
- 了解
社区驱动的商业开发
(CD/CD)。
- 阅读 “面向资源与面向活动的 Web 服务”(developerWorks,2004 年 10 月)以了解 REST 风格与 SOAP 风格的 Web 服务间的关系。
-
本系列的 RSS 提要(了解更多有关 RSS 的内容)。
- 浏览
技术书店
寻找有关这些主题和其他技术主题的书籍。
- 访问
developerWorks 架构专区 获得提升您在 IT 体系结构方面的技能所需的参考资料。
获得产品和技术
-
Project Zero 下载
- 下载
IBM 产品评估版
并着手使用这些来自
DB2®、Lotus®、Rational®、Tivoli® 和
WebSphere 的应用程序开发工具和中间件产品。
讨论

Roland Barcia 是一名 IBM 资深技术人员和 IBM Software Services for WebSphere 的首席 Web 2.0 架构师。他也是 IBM WebSphere: Deployment and Advanced Configuration 和即将发表的 Persistence within the Enterprise 的合著者之一。

Steve Ims 是一名 IBM 资深技术人员和 Project Zero 运行时 “core” 的领头人。在 Project Zero 论坛 上可以找到 Steve。