本文的目的在于说明如何为 LDAP 目录创建可通过 SPARQL 查询的端点,同时介绍了重要的语义 Web 概念。建立端点之后,我还将说明如何使用一些 Jena Java? 类进一步完善,最后使用 JavaScript 从基于浏览器的客户端查询它,
语义 Web 是一种新出现的技术,它以表示数据、查询数据和对数据应用规则的一组标准为基础。核心技术包括用于表示的 RDF、用于查询的 SPARQL、用于构造的 RDFS 以及用于构造和推理的 OWL。语义 Web 有很多突出的优点,包括数据集成更简单、搜索更精确、知识管理更方便等等,结果语义 Web 这个词的含义越来越丰富。
RDF 和 XML 有一些重要的区别。首先,RDF 是基于图的,而 XML 是基于树的。RDF 没有明确的顺序,所有的边组成一个集合,而 XML 元素是有顺序的。最后,RDF 是一种不含标准序列化的数据模型。RDF 可以序列化成多种形式,包括 RDF-XML、n3、Terse RDF Triple Language等。清单 1 中的例子采用 Turtle 描述了关于两个人的一些联系信息。
@prefix foaf: . a foaf:Person ; foaf:name "Wing C. Yung" ; foaf:mbox ; foaf:phone "1-555-555-5555" ; foaf:knows . a foaf:Person ; foaf:name "Lee Feigenbaum" ; foaf:mbox ; foaf:phone "1-555-555-5556" .
第一行定义了数据前缀,这样 DE>DE> 就能简写为 DE>foaf:nameDE>。可以指定多个前缀。此外,Turtle 用分号(DE>;DE>)表示后续行中的谓词和宾语使用相同的主语。谓词 DE>aDE> 是 RDF 类型谓词(DE>DE>)的缩写,可用于表示资源属于特定类型。
使用 SPARQL 查询 RDF
试验下面的查询可使用通用的 SPARQL 处理程序(请参阅 参考资料)。将 DE>http://wingerz.com/dw/listing1.ttlDE> 粘贴到 Target graph URI 字段中。输入查询并单击 Get Results 之后,就会检索 Turtle 文件并进行查询。
DE>SELECTDE>:返回满足查询的一组变量绑定(类似 SQL DE>SELECTDE>)。非常适合产生应用程序要消费的数据。
DE>CONSTRUCTDE>:返回一个图(一组 RDF 命题)。适合检索和转换 RDF。
DE>ASKDE>:返回一个布尔值,说明是否存在查询的结果。
DE>DESCRIBEDE>:依赖于实现。接收一个资源并返回描述该资源的图。
清单 2 示范了 DE>SELECTDE> 查询。
Query: PREFIX foaf: SELECT ?person ?phone WHERE { ?person foaf:name "Wing C. Yung" . ?person foaf:phone ?phone }
Result:
---------------------------------------------------- | person | phone | ==================================================== | | "1-555-555-5555" | ----------------------------------------------------
DE>CONSTRUCTDE> 非常重要,可以根据 SPARQL 查询结果构造 RDF 图。在数据合并的时候很方便,因为 CONSTRUCT 图中的谓词不一定和原图中的相同。清单中查询 DE>foaf:phoneDE>,不过在它的位置使用不同的谓词构造了一个新的图(DE>wingerz:officephoneDE>)。
Query: PREFIX foaf: PREFIX wingerz: CONSTRUCT { ?person foaf:name "Wing C. Yung" . ?person wingerz:officephone ?phone } WHERE { ?person foaf:name "Wing C. Yung" . ?person foaf:phone ?phone }
Result:
1-555-555-5555Wing C. Yung
试验 DE>CONSTRUCTDE> 查询的时候,在资源页面上可以看到完整的响应。遗憾的是,结果被序列化为 RDF-XML 格式,不是很优雅的一种 RDF 序列化格式。
初看起来,RDF 数据似乎有点笨拙而且非常罗嗦。但是有一些突出的优点。
数据的结构没有限制。XML 是层次化的,在关系数据库中建模(和查询)图结构非常困难。
数据容易合并。合并数据(不同的图)是一项简单操作,只需要建立包含图中所有三元组的集合。全球唯一资源减少了歧义。而且如果必要的话,可以用 OWL 规则将不同 URI 的资源映射到同一个 URI。
SquirrelRDF 入门
LDAP 目录本身的结构很容易转化成 RDF。每个 LDAP 对象类都有一组属性。其中一些属性指向文字值(比如名称),另一些则指向其他对象[比如专用名词(DN)指定的工作地点]。
安装后,第一步是寻找 LDAP 存储的模式。如果没有可供试验的 LDAP 存储,可以安装 OpenLDAP 并按照使用说明创建一个简单的地址簿(请参阅 参考资料,尽管可能没有必要费这么多事,因为本文的目标是利用已有的数据源)。模式包含所有不同的对象类及其属性。通过分析属性,可以确定目录中对象之间的关系。
现在来创建从 LDAP 模式到 RDF 的映射。SquirrelRDF 使用 RDF 文件表示这种映射。DE>lmap:serverDE> 谓词用于指定 LDAP 存储的位置。映射允许分配两种类型的 RDF 谓词:文字宾语或者资源宾语。姓名和电话这类基本属性映射为文字宾语谓词。这类映射需要 LDAP 属性名和 RDF 谓词名。然后即可将其链接到资源。假设需要将 LDAP DE>cnDE> 映射到 RDF DE>foaf:nameDE>。清单 4 中创建了一个资源来进行链接 (DE>:namemappingDE>),然后将其链接到配置资源(DE>DE>)。
@prefix foaf: . @prefix lmap: . lmap:mapsProp :namemapping . :namemapping lmap:property foaf:name ; lmap:attribute "cn" .
请注意,将这些链接到一起的 URI 无关紧要,可以改用空白节点,即没有 URI 的资源。它与空白 URI 的资源不同,后面也包括这样的资源。空白 URI 是一种相对 URI,因而解析到 Turtle 文件所在的位置。
清单 5. SquirrelRDF 文字映射到空白节点
@prefix foaf: . @prefix lmap: . lmap:mapsProp [ lmap:property foaf:name ; lmap:attribute "cn" ]
要映射指向专有名词的 LDAP 属性,可将空白节点的类型指定为 DE>lmap:ObjectPropertyDE>。这样可以保证分配的谓词指向资源(而不是文字)。清单 6 显示了完整的配置文件,包括两个 DE>lmap:ObjectPropertyDE> 属性。LDAP 属性默认分配文字资源谓词。要注意,可能的话最好使用已有的谓词,这里使用了 FOAF 本体的一些词汇表。对于地点也有一个词汇表,不过这里没有用到。
@prefix rdf: . @prefix foaf: . @prefix lmap: . @prefix people: . @prefix ol: . a lmap:Map ; lmap:server ; # Person properties lmap:mapsProp [ lmap:property foaf:name ; lmap:attribute "cn" ; ] ; lmap:mapsProp [ lmap:property foaf:phone ; lmap:attribute "telephoneNumber" ; ] ; lmap:mapsProp [ lmap:property people:ol ; lmap:attribute "officeLocation" ; a lmap:ObjectProperty ; ] ; lmap:mapsProp [ lmap:property people:manager ; lmap:attribute "manager" ; a lmap:ObjectProperty ; ] ; # OfficeLocation properties lmap:mapsProp [ lmap:property ol:address1 ; lmap:attribute "address1" ; ] ; lmap:mapsProp [ lmap:property ol:address2 ; lmap:attribute "address2" ; ] ; lmap:mapsProp [ lmap:property ol:city ; lmap:attribute "city" ; ] ; lmap:mapsProp [ lmap:property ol:state ; lmap:attribute "state" ; ] ; lmap:mapsProp [ lmap:property ol:postalCode ; lmap:attribute "postalCode" ; ] ; .
实验映射
清单 7. 测试查询 (test.rq)
PREFIX foaf: SELECT ?person ?phone WHERE { ?person foaf:name "Wing C Yung" . ?person foaf:phone ?phone }
以 DE>dw.ttlDE>(映射文件名)和 DE>test.rqDE>(查询文件名)为参数运行 DE>squirrelrdf.QueryDE>。应该会得到某个人的 URI 和电话号码。
清单 8. 测试查询 (test2.rq)
PREFIX foaf: SELECT ?person ?city ?name WHERE{ ?person foaf:name ?name . ?person people:ol ?officelocation . ?officelocation ol:state "MA" ; ol:city ?city }
将 SquirrelRDF 设置成服务
缺省 Servlet 非常小,只能回答 DE>SELECTDE> 查询,用标准 XML 格式表示结果集。为了使 SPARQL 端点更有用,可让它支持更多的特性。
增加更多的功能之前,我们先要找一种更好的办法测试端点,因为 URL 编码查询和手工创建 URL 都很麻烦。第一个例子使用的通用 SPARQL 处理程序无法再用,因为它取回执行查询的整个 RDF 图,我们的 SquirrelRDF 服务本身是一个能回答查询的 SPARQL 端点(通过将其转化成 LDAP 查询)。创建一个简单 HTML 表单编写和提交查询。一个例子是功能更完善的 SPARQLer(请参阅 参考资料)。单击 Graphs 选项卡来设置端点 endpoint(比如 http://localhost:8080/squirrel)。单击 Change。建立查询提交的端点。尝试本文前面用到的查询。
如前所述,DE>CONSTRUCTDE> 查询很重要,尤其是对非 RDF 格式存储的数据。现在,Servlet 仅支持 DE>SELECTDE> 查询,但是底层的查询引擎可以执行全部四种查询类型。扩展现有的 Servlet 需要修改 DE>doQuery()DE> 方法,如清单 9 所示。DE>com.hp.hpl.jena.query.QueryDE> 对象知道是什么类型的查询,因此可通过它确定 DE>com.hp.hpl.jena.query.engine1.QueryEngineDE> 应该怎么做。不同的查询类型返回不同类型的数据:DE>ASKDE> 返回布尔值,DE>CONSTRUCTDE> 和 DE>DESCRIBEDE> 返回图(从技术上说,返回的是图的包装 DE>ModelDE>),DE>SELECTDE> 返回结果集。
Query q = QueryFactory.create(theQuery, ".", Syntax.syntaxSPARQL); int queryType = q.getQueryType(); Model m = null; switch(queryType){ case Query.QueryTypeAsk: boolean b = qe.execAsk(); String str = ResultSetFormatter.asXMLString(b); resp.setHeader("Content-Type", "application/xml"); resp.getOutputStream().write(str.getBytes()); break; case Query.QueryTypeConstruct: // Gets a model. m = qe.execConstruct(); resp.setHeader("Content-Type", "application/rdf+xml"); // A Model can serialize itself. // The serialization format can be passed in as an argument, // default is to write out as RDF/XML. m.write(resp.getOutputStream()); break; case Query.QueryTypeDescribe: m = qe.execConstruct(); resp.setHeader("Content-Type", "application/rdf+xml"); m.write(resp.getOutputStream()); break; case Query.QueryTypeSelect: ResultSet results = qe.execSelect(); resp.setHeader("Content-Type", "application/xml"); ResultSetFormatter.outputAsXML(resp.getOutputStream(), results); break; }
获得 JSON 输出
清单 10. JSON 查询结果
{ "head": { "vars": [ "person" , "phone" ] } , "results": { "distinct": false , "ordered": false , "bindings": [ { "person": { "type": "uri" , "value": "http://wingerz.com/who#wing" } , "phone": { "type": "literal" , "value": "1-555-555-5555" } } ] } }
DE>com.hp.hpl.jena.query.ResultSetFormatterDE> 工具类用于输出 SPARQL 查询结果。清单 9 使用它以 XML 格式输出结果(DE>SELECTDE> 和 DE>ASKDE> 查询)。结果也可以用 RDF 格式。毫不奇怪,使用这个工具类也能将 DE>SELECTDE> 结果邦定输出为 JSON,如清单 11 所示。
String output = req.getParameter("output"); if (output.equals("json")){ resp.setHeader("Content-Type", "application/json"); ResultSetFormatter.outputAsJSON(resp.getOutputStream(), results); }
支持 XSLT
可以向服务器上增加 XSLT 服务,通过外部 XSLT 服务处理查询结果,或者在 XML 输出重增加样式表链接。在 XML 输出中添加样式表链接,将 DE>com.hp.hpl.jena.query.ResultSetFormatterDE> 作为 XSLT URI 的新增参数传递,如清单 13 所示。这样不会执行转换,只是在生成的文档中包括到 XSLT 的链接。当 XSLT 处理程序(比如现代 Web 浏览器)准备文档的时候就会执行转换了(请参阅 参考资料)。
http://wingerz.com/who#wing1-555-555-5555
清单 13. 包括 XSLT
引用
String stylesheet = req.getParameter("xslt");
if (stylesheet != null)
ResultSetFormatter.outputAsXML(resp.getOutputStream(), results, stylesheet);
else
ResultSetFormatter.outputAsXML(resp.getOutputStream(), results);
if (stylesheet != null)
ResultSetFormatter.outputAsXML(resp.getOutputStream(), results, stylesheet);
else
ResultSetFormatter.outputAsXML(resp.getOutputStream(), results);
从 JavaScript 查询
中提供了可免费下载的 SPARQL JavaScript 库。主要用于处理 SPARQL DE>SELECTDE> 查询返回的 JSON 格式的结果。所幸的是,可以让 SPARQL 服务支持这种功能。该库背后最突出的思想是转换器的概念——SPARQL 端点的 JSON 输出包含一般客户机可能用不到的一些数据,比如某些数据类型信息。转换器将 JSON 输出转换成更容易使用的、更自然的对象。