XQuery/REST 接口定义
REST 接口,特别是用于调用服务的 URL 语言,没有等同于 SOAP 的 WSDL。这个例子探讨了创建用于定义此类接口的简单 XML 模式,以及使用 XQuery 脚本根据接口定义创建通用的网站接口。
以下是使用自制模式对 del.icio.us 接口进行的部分定义。参数使用每个参数的唯一本地名称定义,然后接口支持的服务使用模板定义,大括号用于分隔参数的名称,以便用其实际值替换。
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<name>del.icio.us</name>
<description><p>An almost complete description of the REST interface of the del.icio.us social
bookmark site, excluding services requiring login</p>
<p> Chris Wallace May 2009</p></description>
<endpoint>http://del.icio.us/</endpoint>
<parameters>
<parameter>
<name>user-id</name>
<purpose>User Identifier</purpose>
<default>morelysq</default>
<tag>model</tag>
</parameter>
<parameter>
<name>tag</name>
<purpose>a group of bookmarks</purpose>
<default>xml</default>
<tag>model</tag>
</parameter>
<parameter>
<name>url</name>
<purpose>bookmark</purpose>
<default>http://xml.com/</default>
<tag>model</tag>
</parameter>
<parameter>
<name>tagview</name>
<purpose>the tag list appearance</purpose>
<options>
<option>list</option>
<option>cloud</option>
</options>
<default>list</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>tagsort</name>
<purpose>the order of tags in the tag list</purpose>
<options>
<option>alpha</option>
<option>freq</option>
</options>
<default>list</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>minfreq</name>
<purpose>the minimum frequency of a tag to appear in the tag list</purpose>
<options>
<option>1</option>
<option>2</option>
<option>5</option>
</options>
<default>1</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>bundleview</name>
<purpose>whether bundles are shown</purpose>
<options>
<option>show</option>
<option>hide</option>
</options>
<default>show</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>pageno</name>
<purpose>the page of the bookmark list</purpose>
<format>[0-9]+</format>
<default>1</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>count</name>
<purpose>the number of bookmarks to shown per page</purpose>
<options>
<option>10</option>
<option>25</option>
<option>50</option>
<option>100</option>
</options>
<default>10</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>search</name>
<purpose>search string</purpose>
<tag>ui</tag>
</parameter>
<parameter>
<name>scope</name>
<purpose>search scope</purpose>
<options>
<option>user</option>
<option>all</option>
<option>web</option>
</options>
<default>all</default>
<tag>ui</tag>
</parameter>
<parameter>
<name>helptopic</name>
<purpose>a page of the help manual</purpose>
<default>urlhistory</default>
<tag>help</tag>
</parameter>
</parameters>
<services>
<service>
<template/>
<purpose>Home Page</purpose>
<tag>home</tag>
</service>
<service>
<template>{user-id}</template>
<purpose>View a user's public bookmarks</purpose>
<tag>user</tag>
</service>
<service>
<template>{user-id}?settagview={tagview}&settagsort={tagsort}&setminfreq={minfreq}&setbundleview={bundleview}&page={pageno}&setcount={count}</template>
<purpose>View a user's public bookmarks, controlling its appearance</purpose>
<tag>user</tag>
</service>
<service>
<template>rss/{user-id}</template>
<purpose>Get an RSS feed of a user's bookmarks - limited to the latest 20 items</purpose>
<tag>user</tag>
<tag>RSS</tag>
</service>
<service>
<template>{user-id}/{tag}</template>
<purpose>View a user's bookmarks by tag</purpose>
<tag>user</tag>
<tag>tag</tag>
</service>
<service>
<template>tag/{tag}</template>
<purpose>View tagged bookmarks</purpose>
<tag>tag</tag>
</service>
<service>
<template>network/{user-id}</template>
<purpose>View a user's network and their tags</purpose>
<tag>user</tag>
<tag>network</tag>
</service>
<service>
<template>subscriptions/{user-id}</template>
<purpose>View a user's subscriptions - i.e.watched bookmarks</purpose>
<tag>user</tag>
<tag>subscriptions</tag>
</service>
<service>
<template>for/{user-id}</template>
<purpose>View links suggested to a user</purpose>
<tag>user</tag>
<tag>links</tag>
</service>
<service>
<template>rss/tag/{tag}</template>
<purpose>Get an RSS feed of tagged bookmarks</purpose>
<tag>tag</tag>
<tag>RSS</tag>
</service>
<service>
<template>popular/{tag}</template>
<purpose>View popular tagged bookmarks</purpose>
<tag>tag</tag>
</service>
<service>
<template>popular/</template>
<purpose>View today's popular bookmarks</purpose>
<tag>current</tag>
</service>
<service>
<template>popular/?new</template>
<purpose>View today's new popular bookmarks</purpose>
<tag>current</tag>
</service>
<service>
<template>url?url={url}</template>
<purpose>View Bookmarks for a URL</purpose>
<tag>url</tag>
</service>
<service>
<template>help/</template>
<purpose>Help index</purpose>
<tag>help</tag>
</service>
<service>
<template>help/{helptopic}</template>
<purpose>View a page of the help manual</purpose>
<tag>help</tag>
</service>
<service>
<template>search/?fr=del_icio_us&p={search}&searchtype={scope}</template>
<purpose>Search for string in different scopes</purpose>
<tag>search</tag>
</service>
<service>
<template>rss/</template>
<purpose>RSS hotlist feed</purpose>
<tag>current</tag>
<tag>RSS</tag>
</service>
<service>
<template>rss/tag/{tag}</template>
<purpose>RSS feed for a tag</purpose>
<tag>tag</tag>
<tag>RSS</tag>
</service>
<service>
<template>html/{user-id}/</template>
<purpose>Get a contolled HTML extract of a user's tag</purpose>
<tag>user</tag>
<tag>html</tag>
</service>
</services>
</interface>
XQuery 脚本接受一个名为 uri 的参数,即 XML 接口描述的 uri。该脚本根据此定义创建通用接口,在表单中的值更改并刷新表单时重新生成服务 URL。
declare namespace rest = "http://ww.cems.uwe.ac.uk/xmlwiki/rest";
(: declare global variables :)
declare variable $uri := request:get-parameter("_uri",());
declare variable $index := request:get-parameter("_index","tag");
declare variable $interface := doc(concat($uri,"?r=",math:random()))/interface;
declare function rest:template-parameters($template as xs:string) as xs:string* {
(: parse the template to get the parameters :)
distinct-values(
for $p in subsequence(tokenize($template,"\{"),2)
return substring-before($p,"}")
)
};
declare function rest:parameter-value($name as xs:string) as xs:string? {
let $parameter := $interface/parameters/parameter[name=$name]
return (request:get-parameter($name,$parameter/default),"")[1]
};
declare function rest:replace-template-parameters($template as xs:string, $names as xs:string* ) as xs:string {
(: recursively replace the tempate paramters by their current values :)
if (empty($names))
then $template
else
let $name := $names[1]
let $value := rest:parameter-value($name)
let $templatex :=
if (exists($value))
then replace($template, concat("\{",$name,"\}"),$value)
else $template
return rest:replace-template-parameters( $templatex,subsequence($names,2))
};
(: interface generation :)
declare function rest:parameter-input-field( $parameter as element(parameter) ) as element(span)? {
(: create a parameter field in the parameter input form :)
let $name := $parameter/name
let $value := rest:parameter-value($name)
return
<span class="input">
<label for="{$name}">
{if ($index = "parameter") (: if it the index is by parameter, generate a link to that part of the index :)
then <a href="#{$name}"> {string($name)}</a>
else $name
}
</label>
{if ($parameter/options)
then
<select name="{$name}" title="{$parameter/purpose}" >
{for $option in $parameter/options/option
return
<option value="{$option}" title="{$option/@label}">
{ if ($option= $value)
then attribute selected {"true"}
else ()
}
{string($option)}
</option>
}
</select>
else
<input type="text" name="{$name}" title="{$parameter/purpose}" value="{$value}" size="{string-length($value)+1}"/>
}
</span>
};
declare function rest:parameter-form() {
<form method="post" action="interface.xq">
<div class="subhead"> interface
<div class="group">
<label for="_uri" > uri </label>
<input type="text" name="_uri" value="{$uri}" size="80"/>
</div>
</div>
{for $tag in distinct-values($interface/parameters/parameter/tag)
return
<div>
<div class="subhead">{$tag} </div>
<div class="group">
{ for $parameter in $interface/parameters/parameter[tag=$tag]
return
rest:parameter-input-field($parameter)
}
</div>
</div>
}
<hr/>
Index services by <select name="_index">
{for $index in ("parameter","tag")
return
if ($index = request:get-parameter("index","tag"))
then <option value="{$index}" selected="true"> {$index} </option>
else <option value="{$index}" > {$index} </option>
}
</select>
<hr/><input type="submit" value="refresh"/>
</form>
};
declare function rest:service-link ($service as element(service) )as element(tr) {
<div>
<div class="label">{string($service/purpose)}</div>
{
let $names := rest:template-parameters($service/template)
let $filledTemplate := rest:replace-template-parameters($service/template,$names)
let $uri :=
if (starts-with($service/template,"http://"))
then $filledTemplate
else concat($interface/endpoint,$filledTemplate)
return
<div class="link"><a href="{$uri}">../{$filledTemplate}</a> </div>
}
</div>
};
declare function rest:parameter-index() {
<div id="index">
<h2>Parameter index </h2>
{for $parameter in $interface/parameters/parameter
let $name := $parameter/name
let $match := concat("{",$name,"}")
order by lower-case($name)
return
<div>
<div class="subhead"><a name="{$name}">{string($name)} </a> </div>
<div class="group">
{for $service in $interface//service[contains(template,$match)]
return rest:service-link($service)
}
</div>
</div>
}
</div>
};
declare function rest:tag-index() {
<div id="index">
<h2>Tag index </h2>
{for $tag in distinct-values($interface//service/tag)
order by lower-case($tag)
return
<div>
<div class="subhead" >{$tag}</div>
<div class="group">
{for $service in $interface//service[tag=$tag]
return rest:service-link($service)
}
</div>
</div>
}
</div>
};
declare option exist:serialize "method=xhtml media-type=text/html omit-xml-declaration=no indent=yes
doctype-public=-//W3C//DTD XHTML 1.0 Transitional//EN
doctype-system=http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";
<html>
<head>
<title>Infterface for {string($interface/name)}</title>
<link rel="stylesheet" type="text/css" href="screen.css" />
</head>
<body>
<h1>{string($interface/name)} interface</h1>
{$interface/description/(text(),*)}
<div id="parameters">
{rest:parameter-form()}
</div>
{if (exists($interface))
then
<div id="services">
<h2> Interface properties </h2>
<div class="group">
<div class="label">Interface definition
<div class="link"><a href="{$uri}">{$uri} </a> </div>
</div>
<div class="label">Service endpoint
<div class="link"><a href="{$interface/endpoint}"> {string($interface/endpoint)} </a> </div>
</div>
</div>
{if ($index = "parameter")
then rest:parameter-index()
else if ($index= "tag")
then rest:tag-index()
else ()
}
</div>
else ()
}
</body>
</html>
该脚本使用常见的层状架构,其中低级函数对基本数据模型进行操作,这些函数又用于生成用户界面的函数。最后,生成的 XHTML 中的类和 id 钩子与 CSS 链接起来以设置页面样式。确定使用多少层以及层如何交互是 XQuery 应用程序中的一个核心设计决策,就像其他技术一样。有几个替代方案值得考虑:脚本生成一个中间 XML 结构,该结构在服务器端或客户端使用 XSLT 进行转换;脚本生成一个 XForm 代替 HTML 表单;整个任务由客户端的 JavaScript 处理;客户端 AJAX 与基本 XQuery 脚本交互。处理此设计空间是 Web 开发的挑战之一。
对于在代理服务器内运行的脚本,由于这些脚本位于 UWE 服务器上,因此多次访问 doc() 函数中的同一 URL 将返回缓存的文件。为了打破缓存,会在 URL 中添加一个随机数。
该脚本使用变量声明来定义脚本函数中使用的一些全局变量。全局变量感觉像是回到了 Fortran COMMON 和类似的恐怖,除了这些变量一旦定义就是常量。尽管如此,对这些变量的依赖关系并不显式。另一种方法是显式地将这些数据通过函数传递下去。使用此样式的替代脚本,将构成数据的单个节点传递到单个“对象”中,执行速度慢了几倍,更冗长,而且可以说没有更易理解。
在模板中替换多个参数是一个递归函数,它依次在整个模板中依次替换每个参数。