开发人员:Google
创建支持 Oracle 数据库的 iGoogle 小工具
作者:Luca Mearelli
了解如何为 iGoogle 和 OpenSocial 容器构建小工具以从 Oracle 数据库读取数据。
2009 年 4 月发布
个性化主页中的小工具是传播信息和拓展 Web 应用程序覆盖面的良好渠道。随着我们越来越多地使用“扩展的企业”或 Enterprise 2.0 应用程序,企业开发人员正在寻求新的方式来接触我们的用户,并在用户通过其他方式接收的信息流中进行集成。
在本文中,您将了解如何将 Oracle 数据库(自身支持 HTTP 和 XML)用作 Google 小工具的后端。与此同时,我们将构建几个简单的小工具以从数据库提取数据,并使用 Oracle XML DB 放置相关的静态文件。无需事先了解 Google 小工具规范,所有示例均已在 Oracle 数据库快捷版中进行测试(并可能会在 Oracle 数据库 11g 上运行)。
何谓“小工具”?
小工具是小工具容器(可以是一个个性化启动页面,也可以是社交网上的一个简介页面)中的一个小软件程序。通常,同一页面上会并列存在多个小工具。我将使用 iGoogle(Google 个性化主页)作为小工具容器,但本文大部分内容适用于所有实施 OpenSocial 规范的容器。
在本页面中,您将找到所有实施 OpenSocial API 的容器列表。还有一个开源参考实施:Shinding(可以安装在防火墙内以提供内部网范围内的启动页面)。
注:截至本文撰写之日,OpenSocial API 尚未在所有可以放置小工具的 Google 属性上发布;某些仍然使用所谓的原有 API。然而,这里显示的所有原则和大多数代码都将适用于原有 API,只需略微修改代码(通常只是将 gadgets.* 命名空间改为原来的 _IG_* 命名空间)。
小工具的结构
小工具的核心只是一个 XML 文件,其中包含小工具需求规范、其界面以及实现其行为的 Javascript 代码。我们来看一下可以安装在主页上的 hello world 小工具。
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Hello World!">
<Require feature="opensocial-0.8"/>
</ModulePrefs>
<Content type="html">
Hello, world!( from Oracle XDB :-) )
</Content>
</Module>
开发和部署
要开发和测试一个小工具,只需一个用于编写 xml 文件的文本编辑器以及一个放置该文件的公共空间。稍后,我们将介绍如何将静态文件放到 Oracle 上,但是现在您可以将其放到任何适合的位置(例如,放到 Google 页面上)。
要测试小工具,可以注册 iGoogle 并让您的主页使用 iGoogle 沙盒。启用沙盒后,您可以添加几个有用的小工具开发人员工具,这将有助于您的小工具测试。转至这里并单击添加开发人员工具链接。

要将 hello world 小工具添加到您的页面,只需将 XML 保存到 Web 服务器上的一个文件中,并在开发人员小工具框中输入其 URL(并单击“添加”)。开发期间,最好针对小工具禁用缓存,否则您将无法即时看到对 XML 文件所做的更改。

XML 描述符文件
我们来查看 Hello World 示例的更多详细信息。所有小工具都封装在 Module 标记中,使用 ModulePrefs 标记可以指定将出现在小工具框顶部的标题。在 ModulePrefs 中,可以描述运行小工具所需的特性。
在 hello world 示例中,我们只需要核心的 OpenSocial API。我们可能需要各种不同的特性,例如,能够在运行时设置首选项或者能够动态调整小工具大小。
同样有趣的是,我们可以在 ModulePrefs 内指定需要在小工具启动时预加载一个资源,以便在需要时它确实可用:
<Preload href="http://host/a/remote/resource"/>
小工具还有一个 Content 部分,我们将在此处放置 HTML,而 HTML 将构成界面和 Javascript 以实施小工具的行为。可以考虑使用 Content 部分放置 HTML 页面的“主体”,其中的内容将在页面上的小工具框中编写。
可以在小工具描述符中使用的所有不同选项和标记均可在小工具参考指南中找到(参见底部链接)。
Javascript API
除了静态 HTML 和 XML 之外,小工具开发的所有工作都将使用 Javascript 完成。OpenSocial 实现了一个富 Javascript API,使我们的小工具可以执行与外部服务交互、下载内容、管理用户首选项以及构建复杂界面等操作。
我们来看两个基本的 API,它们几乎在我们将构建的任何小工具中都很有用。第一个是 MiniMessage 特性,它使我们的小工具可以向用户显示一条短消息。我们可以使用以下代码向用户发出错误警报,提示用户更新界面以显示状态消息。
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Hello World (MiniMessage version)">
<Require feature="opensocial-0.8"/>
<Require feature="minimessage"/>
</ModulePrefs>
<Content type="html">
<
要使用该库,我们需要 ModulePrefs 中的 MiniMessage 特性,然后我们可以获取 MiniMessage 对象的一个实例并用它在小工具中显示消息。消息可能:
- 在几秒钟后自动消失,或者
- 呈现一个需要手动关闭的链接,或者
- 一直停留在屏幕上直至以编程方式删除。
默认情况下,消息显示在小工具框顶部,但您也可以指明特定的容器。消息样式可以在 Javascript 代码中更改,也可以使用静态 CSS 更改。
可供小工具使用的最重要特性之一是能够使用 HTTP 协议从不同主机请求信息。我们可以使用该特性构建到远程服务和数据的接口,将各种数据源直接集成到用户的主页上。
下一个示例将显示如何从远程主机请求简单的文件。
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Hello World (remote version)">
<Require feature="opensocial-0.8"/>
<Require feature="minimessage"/>
</ModulePrefs>
<Content type="html">
<![CDATA[
Hello, world!
<script>
function onResponse(data) {
var msg = new gadgets.MiniMessage(__MODULE_ID__);
var statusMsg = msg.createDismissibleMessage(data.text);
}
function makeRequest() {
var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.TEXT;
gadgets.io.makeRequest('http://my_server:8080/igoogle/hello.txt', onResponse, params);
}
gadgets.util.registerOnLoadHandler(makeRequest);
</script>
</Content>
</Module>

函数 makeRequest 用于从给定的 URL 中获取数据。通过向其传递包含有其获取内容的 Javascript 对象,小工具容器从远程服务器获取数据并调用我们指定的回调函数。
{
data : <parsed data, if applicable>,
errors : <any errors that occurred>,
text : <raw text of the response>
}
我们还可以通过指定我们希望从服务器读取的格式,请求容器解析响应。支持的格式包括 JSON、FEED(用于 Atom 或 RSS 源)或 DOM(用于解析到 DOM 对象的通用 XML)。
还可以指定用于进行远程请求的 HTTP 方法,如果远程服务器支持该请求,则请求可以得到签名或得以使用 OAuth 授权委托协议。小工具可以使用 OAuth 安全地获得用户的授权,从而访问自己托管在第三方服务上的数据。(此主题不在本文讨论范围内,但是单击此处可以找到有关在小工具内使用 OAuth 的一些相关信息。)
构建小工具
现在,您将了解如何使用数据库内置的 HTTP 监听器为小工具的 XML 静态文件提供服务,并为查询数据的小工具构建动态响应。
为 XML 提供服务
由于 Oracle XML DB 的推出,Oracle 得以能够直接通过 HTTP 协议提供内容,充当 Web 客户端的信息库。这给了我们一个文件信息库,它具有数据库的所有属性(与备份、安全性、可扩展性等有关的属性),同时简化了使用常见工具(如文本编辑器)的内容管理,因为也可以通过 FTP 和 WebDAV 协议进行访问。
要为 XML 提供服务,我们将创建一个目录来放置我们的小工具文件。为此,我们需要以具有 xdbadmin 角色的用户身份登录到数据库,然后使用 dbms_xdb 程序包的 createFolder 函数。
默认情况下,Oracle 数据库快捷版的 HTTP 监听器处于启用状态,监听端口 8080(如果未启用,我们需要使用 dbms_xdb.setListenerEndPoint 启用它),但是该监听器只能从本地主机访问,因此我们需要支持对 HTTP 监听器的外部访问。这只需调用存储了 FALSE 参数的 setListenerLocalAccess。
DECLARE
v_return BOOLEAN;
BEGIN
-- Create the igoogle folder
v_return := dbms_xdb.createFolder('/igoogle/');
-- enable remote access for the HTTP listener
dbms_xdb.setListenerLocalAccess(l_access => FALSE);
COMMIT;
END;
/
-- make the changes apply immediately
ALTER SYSTEM REGISTER;
该命令在 XML DB 中创建一个 igoogle“目录”,可通过 http://<服务器>:<端口>/igoogle 访问并开放访问,以便我们可以从客户端使用该目录 — 将其作为 WebFolder 安装到 Windows 中(或者作为共享文件夹安装到 Mac OS X 中)— 并且 iGoogle 容器可以访问我们存储在其中的文件(如果主机/端口可通过网络进行公共访问)。
要测试该设置,只需将示例文件复制到共享 igoogle 文件夹,然后使用以下 URL 将其添加到 iGoogle 页面:http://<服务器>:<端口>/igoogle/<示例文件名称>.xml。
查询数据源
现在该看看如何将小工具连接到数据源,服务于从数据库提取的信息。
数据库作为直接信息源使我们可以灵活地使用管理其余数据管理任务的工具来管理它;例如,通过将负责 http 数据库访问的用户与持有实际数据的模式隔离开来,可以对权限进行细粒度调整。
我们将创建一个特定于 iGoogle 小工具的 IG 用户并仅赋予其对数据的读取权限(使用示例中的 HR 演示模式)。为响应小工具请求而从 http 监听器调用的所有 PL/SQL 代码和查询将使用 IG 用户权限执行,因此,小工具将无法更改数据库中的数据,而只能读取。我们不在这里显示代码,但是可以在本文的示例代码下载中找到。
Oracle 数据库 10g 第 2 版之后的另一个重要元素是嵌入式 PL/SQL 网关,它允许使用 PL/SQL 创建 Web 应用程序。它提供了一种从 HTTP 客户端调用存储过程的方法,并具有一个程序包可以简化 HTML 或其他内容的生成和返回。
DBMS_EPG 可用于管理嵌入式 PL/SQL 网关。
我们将定义一个数据库访问描述符 (DAD),将 HTTP 请求传递到与其相关的指向 IG 模式存储过程的虚拟路径。
-- create the DAD with an associated virtual path
BEGIN
DBMS_EPG.create_dad (
dad_name => 'igoogle/apps',
path => '/igoogle/apps/*');
END;
/
-- set the DAD database username to be used
-- this avoids requiring the user / password
-- via HTTP BasicAuth
BEGIN
DBMS_EPG.set_dad_attribute (
dad_name => 'igoogle/apps',
attr_name => 'database-username',
attr_value => 'IG');
END;
/
-- enable access to the specified schema via the DAD
BEGIN
DBMS_EPG.authorize_dad (
dad_name => 'igoogle/apps',
user => 'IG');
END;
/
通过这种方式,我们将能够按照以下简单的模式从 iGoogle 小工具中调用 IG 模式中的存储过程:http://<服务器>:<端口>/igoogle/apps/<程序包名称>.<存储过程名称>
要对其进行测试,创建以下返回字符串 Hello world! 的存储过程:
CREATE OR REPLACE PROCEDURE ig.hello_world IS
BEGIN
htp.print('Hello world!');
END hello_world;
/
SHOW ERRORS
并在一个打开以下 URL 的浏览器中调用该存储过程:http://<服务器>:<端口>/igoogle/apps/hello_world
将数据发送到客户端
如您所见,小工具可以使用远程服务器中以各种格式提供的数据,而 XML 和 JSON 数据最为有用。在存储过程中,使用 DBMS_XMLGEN 程序包从任意查询生成 XML 数据确实很容易,以下示例将查询 HR 模式中的 regions 表并将数据返回客户端:
procedure regions is
l_ctx dbms_xmlgen.ctxHandle;
l_xml clob;
begin
l_ctx := dbms_xmlgen.newContext('select * from hr.regions');
l_xml := dbms_xmlgen.getXML(l_ctx);
ig.prn_clob(l_xml);
dbms_xmlgen.closecontext(l_ctx);
end regions;
DBMS_XMLGEN 过程返回包含 XML 内容的 CLOB,但是 htp 过程要求向其传递字符串参数,因此我们需要通过若干小块发送回复。为此,我们使用一个简单的存储过程,封装对 htp.prn 的调用并允许我们发送通用 CLOB 的内容。
create or replace procedure ig.prn_clob (p_clob in clob)
as
offset number := 1;
amount number := 4000;
len number := dbms_lob.getlength (p_clob);
lc_buffer varchar2 (4000);
begin
while (offset < len) loop
dbms_lob.read (p_clob, amount, offset, lc_buffer);
htp.prn (lc_buffer);
offset := offset + amount;
end loop;
end prn_clob;
XML 具有规范格式:
<?xml version="1.0"?>
<ROWSET>
<ROW>
<REGION_ID>1</REGION_ID>
<REGION_NAME>Europe</REGION_NAME>
</ROW>
<ROW>
<REGION_ID>2</REGION_ID>
<REGION_NAME>Americas</REGION_NAME>
</ROW>
<ROW>
<REGION_ID>3</REGION_ID>
<REGION_NAME>Asia</REGION_NAME>
</ROW>
<ROW>
<REGION_ID>4</REGION_ID>
<REGION_NAME>Middle East and Africa</REGION_NAME>
</ROW>
</ROWSET>
接下来将介绍一个使用 makeRequest 小工具 API 查询远程数据库的小工具。请求回调收到从 XML 生成的解析 DOM 对象,小工具对其进行遍历(读取每个区域名称和 ID)并构建区域列表(然后在其中写入小工具)。
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Show the regions">
<Require feature="opensocial-0.8"/>
<Require feature="minimessage"/>
<Require feature="dynamic-height"/>
</ModulePrefs>
<Content type="html">
<![CDATA[
<div id='regions'></div>
<div id='reload' style="display:none;"><a href='javascript:void(0);' onclick="init(); return false;">Click here to reload</div>
<script>
var regionsList;
var msg = new gadgets.MiniMessage(__MODULE_ID__);
var statusMsg;
function noDataFound(errors) {
if (errors && errors.length) {
statusMsg = msg.createTimerMessage("Error getting data:"+errors.join(", "), 5);
statusMsg.style.backgroundColor = "red";
statusMsg.style.color = "white";
} else {
statusMsg = msg. createDismissibleMessage("No data found.");
}
}
function onRegions(data) {
msg.dismissMessage(statusMsg);
if (data.data) {
regionsList = data.data.getElementsByTagName("ROW");
} else {
noDataFound(data.errors);
document.getElementById('reload').style.display = "block";
return;
}
var html = new Array();
html.push('<i>Regions in the database:</i><br/>')
html.push('<ul>');
for (var i = 0; i < regionsList.length; i++) {
html.push('<li ');
var region_id = regionsList[i].getElementsByTagName("REGION_ID");
if (region_id && region_id.length) {
html.push('id="region_', region_id[0].textContent, '"');
}
html.push('>');
var region_name = regionsList[i].getElementsByTagName("REGION_NAME");
if (region_name && region_name.length) {
html.push(region_name[0].textContent);
}
html.push('</li>');
}
html.push('</ul>');
document.getElementById('regions').innerHTML = html.join('');
gadgets.window.adjustHeight();
}
function requestRegions() {
var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.DOM;
gadgets.io.makeRequest('http://my_server:8080/igoogle/apps/hrgadget.regions', onRegions, params);
}
function init() {
statusMsg = msg.createStaticMessage("Loading the regions");
document.getElementById('regions').innerHTML = '';
document.getElementById('reload').style.display = "none";
requestRegions();
}
gadgets.util.registerOnLoadHandler(init);
</script>
</Content>
</Module>

除了 DOM 查询外,需要注意的是在请求或解析响应进行中检测到错误的情况下代码段的使用,查看 Javascript 代码中的 noDataFound 函数以了解错误如何写入 MiniMessage。
生成 JSON 回复
通过以上示例可以清楚地看出,使用默认的 XML 格式(虽然从服务器生成很简单)需要在小工具中处理相当冗长的代码。但是通过在存储过程中生成一个 JSON 应答并从小工具发出 gadgets.io.ContentType.JSON 请求,就可以轻松解决此问题。
幸运的是,DBMS_XMLGEN 程序包提供了使用 XSL 表修改返回的 XML 的非常简单的方法,虽然该方法通常用于将一种 XML 格式转换成另一种 XML 格式,但我们也可以使用它从 XML 构建 JSON 消息。
要进行此操作,只需使用 setXslt 过程向其传递针对查询生成的 DBMS_XMLGEN 内容以及作为 XMLType 对象的 XSL 表。这样,getXML 函数将返回 XSL 表转换的 XML。
procedure regions_json is
l_ctx dbms_xmlgen.ctxHandle;
l_xml clob;
l_xsl XMLType := XMLType(q'[
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" method="text" encoding="UTF-8" media-type="text/x-json"/>
<xsl:template match="/">
{ "regions" : [
<xsl:for-each select="ROWSET/ROW">
<xsl:if test="position() > 1">,</xsl:if>
{ "id" :"<xsl:value-of select="REGION_ID"/>",
"name" :"<xsl:value-of select="REGION_NAME"/>"
}
</xsl:for-each>
]
}
</xsl:template>
</xsl:stylesheet>]'
);
l_output XMLType;
l_xml_data XMLType;
begin
l_ctx := dbms_xmlgen.newContext('select * from hr.regions');
dbms_xmlgen.setnullhandling(l_ctx, 1);
dbms_xmlgen.setxslt(l_ctx, l_xsl);
l_xml := dbms_xmlgen.getXML(l_ctx);
OWA_UTIL.mime_header ('text/x-json', FALSE);
htp.p('Cache-Control:no-cache');
htp.p('Pragma:no-cache');
OWA_UTIL.http_header_close;
ig.prn_clob(dbms_xmlgen.convert(l_xml,1));
dbms_xmlgen.closecontext(l_ctx);
end regions_json;
我们使用 OWA_UTIL.mime_header 过程为 JSON 响应设置正确的 mime 类型。我们还需要在 getXML 返回已实体编码的文本时结束文本引用。这将通过转换函数完成。以下是返回的内容:
{ "regions" : [
{ "id" : "1",
"name" : "Europe"
},
{ "id" : "2",
"name" : "Americas"
},
{ "id" : "3",
"name" : "Asia"
},
{ "id" : "4",
"name" : "Middle East and Africa"
}
]
}
当在小工具中对其进行解析时,我们获取了一个简单的 Javascript 对象,该对象具有一个 regions 属性,包含一个“region”对象数组,每个对象都具有自己的 ID 和名称属性,从而使小工具代码更简单、更易于理解。
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Show the regions (json version)">
<Require feature="opensocial-0.8" />
<Require feature="minimessage"/>
</ModulePrefs>
<Content type="html">
<![CDATA[
<div id='main'>
<i>Regions in the database:</i>
<div id='regions'</div>
<div id='reload' style="display:none;"><a href='javascript:void(0);' onclick="init(); return false;">Click here to reload</div>
</div>
<script>
var regionsList = {};
var msg = new gadgets.MiniMessage(__MODULE_ID__);
var statusMsg;
function noDataFound(errors) {
if (errors && errors.length) {
statusMsg = msg.createTimerMessage("Error getting data: "+errors.join(", "), 5);
statusMsg.style.backgroundColor = "red";
statusMsg.style.color = "white";
} else {
statusMsg = msg. createDismissibleMessage("No data found.");
}
}
function onRegions(data) {
msg.dismissMessage(statusMsg);
if (data.data && data.data.regions && data.data.regions.length) {
regionsList = data.data.regions;
} else {
noDataFound(data.errors);
document.getElementById('reload').style.display = "block";
return;
}
var html = new Array();
html.push('<ul>');
for (var i = 0; i < regionsList.length; i++) {
html.push('<li id="region_', regionsList[i].id, '">', regionsList[i].name, '</li>');
}
html.push('</ul>');
document.getElementById('regions').innerHTML = html.join('');
}
function requestRegions() {
var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
gadgets.io.makeRequest('http://my_server:8080/igoogle/apps/hrgadget.regions_json', onRegions, params);
}
function init() {
statusMsg = msg.createStaticMessage("Loading the regions");
document.getElementById('regions').innerHTML = '';
document.getElementById('reload').style.display = "none";
requestRegions();
}
gadgets.util.registerOnLoadHandler(init);
</script>
</Content>
</Module>

向查询传递参数
使用此处描述的方法将参数传递给存储过程确实很简单。只需创建一个接受输入参数的存储过程,其中每个参数将是一个应该通过 HTTP 传入的查询参数。以下过程读取给定区域(其 ID 作为“r”输入参数传递)的国家/地区列表:
procedure countries_for_region_json(r in number) is
l_ctx dbms_xmlgen.ctxHandle;
l_xml clob;
l_xsl XMLType := XMLType(q'[<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" method="text" encoding="UTF-8" media-type="text/x-json"/>
<xsl:template match="/">
{ "countries" : [
<xsl:for-each select="ROWSET/ROW">
<xsl:if test="position() > 1">,</xsl:if>
{
"id" :"<xsl:value-of select="COUNTRY_ID"/>",
"name" :"<xsl:value-of select="COUNTRY_NAME"/>",
"has_departments" :<xsl:choose><xsl:when test="DEP_CNT > 0">true</xsl:when><xsl:otherwise>false</xsl:otherwise></xsl:choose>
}
</xsl:for-each>
] }
</xsl:template>
</xsl:stylesheet>]'
);
l_output XMLType;
l_xml_data XMLType;
l_srf SYS_REFCURSOR;
begin
open l_srf for
select c.*, nvl(cd.dep_cnt,0) dep_cnt
from hr.countries c
left outer join
( select count(*) dep_cnt, l.country_id
from hr.departments d
join hr.locations l on l.location_id = d.location_id
GROUP by l.country_id
) cd on cd.country_id = c.country_id
where c.region_id = r;
l_ctx := dbms_xmlgen.newContext(l_srf);
dbms_xmlgen.setnullhandling(l_ctx, 1);
dbms_xmlgen.setxslt(l_ctx, l_xsl);
l_xml := dbms_xmlgen.getXML(l_ctx);
OWA_UTIL.mime_header ('text/x-json', FALSE);
htp.p('Cache-Control:no-cache');
htp.p('Pragma:no-cache');
OWA_UTIL.http_header_close;
ig.prn_clob(dbms_xmlgen.convert(l_xml,1));
dbms_xmlgen.closecontext(l_ctx);
end countries_for_region_json;
要调用此传递区域 ID 的过程,我们只需通过 HTTP 访问该过程的 URL:
http://<server>:<port>/igoogle/apps/hrgadget.countries_for_region_json?r=<region id>
在以下示例小工具中,我们在加载小工具时从数据库中的请求区域列表,然后请求国家列表(如以上示例所示),但是现在该列表内容已增加,为每个区域创建了一个链接用以调用 Javascript 函数,请求该区域的国家/地区列表。当接收到国家/地区列表时,将隐藏区域列表,我们将显示刚刚从数据库中读取的国家/地区。
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="From regions to countries">
<Require feature="opensocial-0.8" />
<Require feature="minimessage"/>
<Require feature="dynamic-height"/>
</ModulePrefs>
<Content type="html">
<![CDATA[
<div id='regions' style="display:none;"></div>
<div id='countries' style="display:none;"></div>
<div id='reload' style="display:none;"><a href='javascript:void(0);' onclick="requestRegions(); return false;">Click here to reload</div>
<script>
var regionsList;
var msg = new gadgets.MiniMessage(__MODULE_ID__);
var statusMsg;
var current_region_id;
function noDataFound(errors) {
if (errors && errors.length) {
statusMsg = msg.createTimerMessage("Error getting data: "+errors.join(", "), 5);
statusMsg.style.backgroundColor = "red";
statusMsg.style.color = "white";
} else {
statusMsg = msg. createDismissibleMessage("No data found.");
}
}
function show(what) {
if (what == 'countries') {
document.getElementById('regions').style.display = "none";
document.getElementById('reload').style.display = "none";
document.getElementById('countries').style.display = "block";
} else {
document.getElementById('regions').style.display = "block";
document.getElementById('reload').style.display = "none";
document.getElementById('countries').style.display = "none";
}
}
function onRegions(data) {
msg.dismissMessage(statusMsg);
if (data.data && data.data.regions && data.data.regions.length) {
regionsList = data.data.regions;
} else {
noDataFound(data.errors);
document.getElementById('reload').style.display = "block";
return;
}
var html = new Array();
html.push('<i>Regions in the database:</i><br/><small>Click to on the name to load the countries in that region.</small>')
html.push('<ul>');
for (var i = 0; i < regionsList.length; i++) {
html.push('<li>');
html.push('<a href="javascript:void(0);" onclick="requestCountries(\'', regionsList[i].id ,'\'); return false;" ',
' id="region_', regionsList[i].id, '"', '>');
html.push(regionsList[i].name);
html.push('</a>');
html.push('</li>');
}
html.push('</ul>');
document.getElementById('regions').innerHTML = html.join('');
show('regions');
gadgets.window.adjustHeight();
}
function requestRegions() {
document.getElementById('regions').innerHTML = '';
statusMsg = msg.createStaticMessage("Loading the regions");
var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
gadgets.io.makeRequest('http://my_server:8080/igoogle/apps/hrgadget.regions_json', onRegions, params);
}
function onCountries(data) {
msg.dismissMessage(statusMsg);
if (data.data && data.data.countries && data.data.countries.length) {
countriesList = data.data.countries;
} else {
noDataFound(data.errors);
return;
}
var html = new Array();
html.push('<i>Countries in ', document.getElementById("region_" + current_region_id).innerHTML, ':</i>');
html.push('<br/><small>The bold ones indicate those where departments are located</small>');
html.push('<ul>');
for (var i = 0; i < countriesList.length; i++) {
html.push('<li id="region_', countriesList[i].id, '" >');
if (countriesList[i].has_departments) {
html.push('<b>');
}
html.push(countriesList[i].name);
if (countriesList[i].has_departments) {
html.push('</b>');
}
html.push('</li>');
}
html.push('</ul>');
html.push('<br/><a href="javascript:void(0);" onclick="show(\'regions\'); gadgets.window.adjustHeight(); return false;">← Regions</a>');
document.getElementById('countries').innerHTML = html.join('');
show('countries');
gadgets.window.adjustHeight();
}
function requestCountries(region_id) {
current_region_id = region_id;
document.getElementById('countries').innerHTML = '';
statusMsg = msg.createStaticMessage("Loading the countries");
var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
gadgets.io.makeRequest('http://my_server:8080/igoogle/apps/hrgadget.countries_for_region_json?r=' + region_id, onCountries, params);
}
gadgets.util.registerOnLoadHandler(requestRegions);
</script>
</Content>
</Module>


可以对这个这个小示例进行扩展,以便在 HR 模式中导航具体到每个员工的全部信息。(在本文的示例代码中,我也包括了这一示例,请求 XML 而不是 JSON 数据,以向您显示使用服务器回复时两种情况之间的差异。)
管理用户首选项
最后,您将了解如何将用户首选项与远程数据服务相结合,在小工具中预填充用户的相关信息。
小工具规范允许使用 UserPref XML 标记定义用户首选项。如果页面重新加载也将保留用户设置的首选项,因此可以针对查看小工具的特定用户对小工具进行自定义。可以定义各种首选项:字符串、布尔型、枚举型、列表(根据用户输入动态生成)以及隐藏不可见或者用户不可编辑的字符串。
如果需要存储更复杂的首选项,建议您使用隐藏首选项将其存储为序列化的 Javascript 对象(以 JSON 格式),本示例就将这样做。
此处,我们从数据库请求部门列表,并要求用户从中选择一个列表(如果尚未进行此选择)。当用户选择部门后,我们查询服务器以获取在该部门工作的员工列表,并向小工具用户显示包含其相关信息的列表。我们将 Javascript 对象也存储在隐藏首选项中(以序列化 JSON 格式),如果首选项已设置,当小工具页面重新加载时,我们将跳过部门选择页面。
在小工具 XML 中使用以下标记定义首选项:
<UserPref name="choosen_dept" datatype="hidden"/>
首选项对象使用以下调用进行实例化(__MODULE_ID__ 由包含可识别小工具字符串的容器替代):
var prefs = new gadgets.Prefs(__MODULE_ID__);
To save the preference programmatically we need to require the setprefs feature, then we are able to use the set method:
prefs.set('choosen_dept', gadgets.json.stringify(choosen_dept));
如您所见,小工具的 API 具有可从对象生成 JSON 并从 JSON 字符串获取对象的实用程序方法:
choosen_dept = gadgets.json.parse(gadgets.util.unescapeString(prefs.getString("choosen_dept")));
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Employees" height="300" >
<Require feature="opensocial-0.8"/>
<Require feature="minimessage"/>
<Require feature="setprefs"/>
<Require feature="settitle"/>
<Preload href="http://my_server:8080/igoogle/apps/hrgadget.departments_json" />
</ModulePrefs>
<UserPref name="choosen_dept"
datatype="hidden" />
<Content type="html">
<![CDATA[
<div id='departments' style="display:none;"></div>
<div id='employees' style="display:none;">
<div id='employees_list' style="width:100%;height:260px;overflow-y:scroll;"> </div>
<hr/>
<small id=''><a href="javascript:void(0);" onclick="loadDepartments();return false;">change department</a></small>
</div>
<script>
// Get userprefs
var prefs = new gadgets.Prefs(__MODULE_ID__);
var choosen_dept;
var departmentsList;
var msg = new gadgets.MiniMessage(__MODULE_ID__);
var statusMsg;
function noDataFound(errors) {
if (errors && errors.length) {
statusMsg = msg.createTimerMessage("Error getting data:"+errors.join(", "), 5);
statusMsg.style.backgroundColor = "red";
statusMsg.style.color = "white";
} else {
statusMsg = msg. createDismissibleMessage("No data found.");
}
}
function show(what) {
if (what == 'departments') {
document.getElementById('departments').style.display = "block";
document.getElementById('employees').style.display = "none";
} else if (what == 'employees') {
document.getElementById('departments').style.display = "none";
document.getElementById('employees').style.display = "block";
} else {
document.getElementById('departments').style.display = "none";
document.getElementById('employees').style.display = "none";
}
}
function onDepartments(data) {
msg.dismissMessage(statusMsg);
if (data.data && data.data.departments && data.data.departments.length) {
departmentsList = data.data.departments;
var html = new Array();
html.push("Choose a department: ")
html.push("<select id=\"dept_select\">")
html.push('<option value=\"\"></option>');
for (var i = 0; i < departmentsList.length; i++) {
html.push('<option value=\"',i,'\">');
html.push(departmentsList[i].name);
html.push('</option>');
}
html.push("</select>");
document.getElementById('departments').innerHTML = html.join('');
document.getElementById('dept_select').onchange = chooseDepartment;
show('departments');
} else {
noDataFound(data.errors);
return;
}
}
function loadDepartments() {
statusMsg = msg.createStaticMessage("Loading data...");
document.getElementById('departments').innerHTML = '';
var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
gadgets.io.makeRequest('http://my_server:8080/igoogle/apps/hrgadget.departments_json', onDepartments, params);
}
function chooseDepartment() {
var dept_idx = document.getElementById('dept_select').value;
if (departmentsList && departmentsList[dept_idx]) {
choosen_dept = departmentsList[dept_idx];
prefs.set('choosen_dept',
gadgets.json.stringify(choosen_dept));
}
loadEmployees();
}
function setChoosenDepartment() {
try {
choosen_dept = gadgets.json.parse(gadgets.util.unescapeString(prefs.getString("choosen_dept")));
} catch (err) {
choosen_dept = null;
}
}
function onEmployees(data) {
msg.dismissMessage(statusMsg);
if (data.data && data.data.employees && data.data.employees.length) {
var employees = data.data.employees;
var html = new Array();
for (var i = 0; i < employees.length; i++) {
html.push("<div class=\"emp\" id=\"emp_",employees[i].id,"\">")
html.push('<b>', employees[i].name, '</b>');
html.push("<div style=\"font-size:10px;\">");
html.push("<i>",employees[i].job_title,"</i><br/>");
html.push("Phone:", employees[i].phone_number, " - <a href=\"mailto:", employees[i].email,"\">send email</a><br/>");
html.push("Managed by:", employees[i].manager,"<br/>");
html.push('</div>');
html.push("</div>");
}
document.getElementById('employees_list').innerHTML = html.join('');
show('employees');
} else {
noDataFound(data.errors);
return;
}
}
function loadEmployees() {
if (!choosen_dept) {
init();
return;
}
document.getElementById('employees_list').innerHTML = '';
gadgets.window.setTitle("Employees of:"+choosen_dept.name);
statusMsg = msg.createStaticMessage("Loading data...");
var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
gadgets.io.makeRequest('http://my_server:8080/igoogle/apps/hrgadget.employees_json?d='+choosen_dept.id, onEmployees, params);
}
function init() {
setChoosenDepartment();
if (choosen_dept) {
loadEmployees();
} else {
loadDepartments();
}
}
gadgets.util.registerOnLoadHandler(init);
</script>
</Content>
</Module>


这个小工具使用的存储过程包含在本文示例代码的 setup_package.sql 文件中。
结论
您现在应该基本了解什么是 iGoogle/OpenSocial 小工具以及如何构建该工具以将 Oracle 数据库用作其数据源。这对于扩展我们企业系统的覆盖面并为我们的用户创建新的与驻留在数据库的数据进行交互的方法极为有用。
您了解了如何在 Oracle 数据库上全面托管小工具以及如何使用数据库 XML 和 HTTP 特性构建在小工具中发布信息所需的数据服务。
最后请注意:在本文发布前不久,Google 安全数据连接器已发布,从而使企业可以安全地从 Google 托管的小工具中开放对其内部网的访问,做法是,允许企业限制可以请求内部服务的用户和应用程序。这使得在企业环境中使用小工具变得更加方便。
参考资料
Luca Mearelli (l.mearelli@spazidigitali.com) 是 Oracle 和 Web 技术专家 (http://spazidigitali.com),住在意大利的 Città di Castello。
|