RDF API – Jena 手册

概述

资源描述框架(RDF)是一套万维网委员会(W3C)推荐的描述资源的标准。什么是资源?这实在是一个很有深度的问题并且对它的准确定义也一直是一个争辩的论题。对我们来说,它是所有我们能够识别的东西。比如,你是一个资源,同样你的个人主页、这篇教程、数字1和Moby Dick的《The Greath White》。

这篇教程,我们使用的所有例子都是关于人的,介绍了一个使用RDF做VCARDS陈述的方式。RDF的表现格式最好是通过节点和弧构成的图,如下所示,一个简单的vcard在RDF里面可以看成这样:

figure 1

资源(Resource) “John Smith”,在椭圆里显示并通过一个统一资源标识符(Uniform Resource Identifier, URI)来表示,如”http://…/JohnSmith”。当然,如果你尝试通过浏览器接入这个资源,你是不会成功的;如果你的浏览器能正确显示这个结果,我们都会被大吃一惊。如果你不熟悉URI,可以把他们简单的想成是一个奇怪的名字。

资源是有属性(Property) 的。在下面的例子中,我们感兴趣的是那些可能出现在John Smith名片上的属性。在前面的图中,唯一的属性vcard:FN,指的是John Smith的全名。像这个属性一样,一个属性是通过一个被属性名“vcard:FN”标注的弧来表示的。属性名同样也是一个 URI(http://www.w3.org/2006/vcard/ns#FN) ,但是由于 URL 很长、很笨重,所以在图中显示的是它的 QNAME 格式。冒号“:”前面的部分叫做命名空间前缀,用来表示一个命名空间;后面部分称为本地名字,用来表示在这个命名空间中的一个名字,QNAME的格式是:
命名空间前缀:本体名字(nsprefix:localname)
命名空间前缀是命名空间的URI的一个缩写,它连接到命名空间中的一个名字,这里和XML的namespace的概念一样。
vcard -> http://www.w3.org/2006/vcard/ns#
vcard:FN -> http://www.w3.org/2006/vcard/ns#FN
在RDF中并没有规定属性的URI通过浏览器能解析出什么东西。QNAME格式表示的属性常常用在写RDF XML,同时在画图表或写文本的时候,它也是一种很方便的速记方式。

每个属性都会有值(Object),在这个例子中,这个值是一组文字(Literal,关于这个的具体介绍可以参考本文最后的内容),它显示在上图的长方形中。

OK,到这里我们简单学习了在RDF中什么是资源(Resource)、属性(Property)和属性的值(Object)。接下来我们来了解一下什么是Jena?

Jena 是用来建立和控制像上面这样的RDF图的一个 Java API (Java Application programming Interface) 。Jena 有一些列的接口用来表示资源、属性和文字,他们分别对应 Resource 、 Property 和 Literal 。在 Jena 中,“图”被当成一个“模型(Model)”,他通过 Model 接口来表示。

下面的代码是用来创建一个图或者说模型,很简单的:

// 一些初始化的定义
static String personURI = "http://somewhere/JohnSmith";
static String fullName = "John Smith";

// 创建一个空的模型
Model model = ModelFactory.createDefaultModel();

// 创建一个资源
Resource johnSmith = model.createResource(personURI);

// 添加一个属性
johnSmith.addProperty(VCARD.FN, fullName);

先声明几个变量,再创建一个空的模型Model,这是使用 ModelFactory 里的 createDefaultModel() 方法创建的一个空的基于内存模型。Jena 还包括了其他方式的模型接口,例如,关系数据库,这些类型的模型同样也存在 ModelFactory 中。

创建模型之后,建立了 John Smith 的资源,并且添加了它的属性。这个属性是通过在 VCARD 中定义好的类操作的,该类的所有对象的定义都在 VCARD schema 中定义 了。Jena 也提供了其他一些有名的 schema(模式),比如 RDF 、 RDF schema 、 Dublin Core 和 OWL 。

在书写上,创建资源和添加属性的代码可以使用更加紧凑的层叠方式来写:

Resource johnSmith =
        model.createResource(personURI)
             .addProperty(VCARD.FN, fullName);

接下去让我们使用 RDF 和 Jena 中丰富的特色来添加更详细的信息到 vcard 里。

在第一个例子里面,属性值是一段文本(Literal)。RDF 属性其实也可以使用其他的资源作为它的值。下面的这个例子使用了 RDF 中比较常见的技术来表示 John Smith 名字里的不同的部分:

figure 2

我们增加了一个新的属性,vcard:N,来表示 John Smith 名字的结构。在这个模型里面,有一些很有趣的东西,vcard:N 属性使用了一个资源作为他的值,但是这个椭圆里面并没有URI,他被认为是一个“空白节点”。

下面的 Jena 代码实现了这个例子,也是非常简单。和前例一样,它先声明和创建一个空的模型。

// 声明变量
String personURI    = "http://somewhere/JohnSmith";
String givenName    = "John";
String familyName   = "Smith";
String fullName     = givenName + " " + familyName;

// 创建一个空的模型
Model model = ModelFactory.createDefaultModel();

// 创建资源
// 并且按照层叠式添加属性。
// model.createResource() 创建了一个空白节点
Resource johnSmith
  = model.createResource(personURI)
         .addProperty(VCARD.FN, fullName)
         .addProperty(VCARD.N,
                      model.createResource()
                           .addProperty(VCARD.Given, givenName)
                           .addProperty(VCARD.Family, familyName));

陈述(Statement)

每一个弧在 RDF 模型中是一个陈述(Statement)。每一个陈述声明了和一个资源相关的一个事实。所以一个陈述的组成分为三个部分:

  1. 第一部分作为主语(Subject) 的资源,在弧的起点位置;
  2. 第二部分作为谓语(Predicate) 的属性,弧的标签;
  3. 第三部分作为宾语(Object) 的资源或者文字,在弧的终点位置。

一个陈述又被叫做“三元组(Triple)”,因为它的三个组成部分。
Subject -> Predicate -> Object

RDF模型表示的是一系列陈述的集合。在上面的例子中,每次使用 addProperty 等于添加一个陈述到模型中(因为模型是一个集合,添加重复的陈述是没有任何效果的)。Jena Model 的接口定义了一个 listStatements() 的方法,它返回的是一个迭代器“StmtIterator”,它在 Jena Model 里覆盖所有陈述,是 Java 迭代器 Iterator的子类。“StmtIterator”有一个方法“nextStatement()”从迭代器里面返回下一个陈述,类似于next()。“陈述”在 Jena 中的接口”Statement”,它给包含主语、谓语、宾语的陈述提供了一个存储器。

下面我们将会使用这个接口去扩展上面的例子来列出所有创建的陈述,并且打印出来。

// 列出模型中所有的陈述,model是上面例子中的model
StmtIterator iter = model.listStatements();

// 打印出每一个陈述的主语、谓语和宾语
while (iter.hasNext()) {
    Statement stmt      = iter.nextStatement();  // 获取下一个陈述
    Resource  subject   = stmt.getSubject();     // 获取主语
    Property  predicate = stmt.getPredicate();   // 获取谓语
    RDFNode   object    = stmt.getObject();      // 获取宾语

    System.out.print(subject.toString());
    System.out.print(" " + predicate.toString() + " ");
    if (object instanceof Resource) {
       System.out.print(object.toString());
    } else {
        // 宾语是文字
        System.out.print(" \"" + object.toString() + "\"");
    }

    System.out.println(" .");
}

由于陈述中的宾语可以是一个资源也可以是一个文字,所以 getObject() 方法返回一个对象类型是 RDFNode ,这个对象是 Resource 和 Literal 的超类(父类)。他的底层对象是一个具体的类型,因此代码中使用了“instanceof”来确定对象类型并用合适的方式去处理。

运行代码后,程序应该生成一个类似于下面的结果:

http://somewhere/JohnSmith http://www.w3.org/2001/vcard-rdf/3.0#N anon:14df86:ecc3dee17b:-7fff .
anon:14df86:ecc3dee17b:-7fff http://www.w3.org/2001/vcard-rdf/3.0#Family  "Smith" .
anon:14df86:ecc3dee17b:-7fff http://www.w3.org/2001/vcard-rdf/3.0#Given  "John" .
http://somewhere/JohnSmith http://www.w3.org/2001/vcard-rdf/3.0#FN  "John Smith" .

看到这个结果是不是觉得很复杂,如果你仔细看,你将会看见这个结果的每一行都有三块分别表示一个陈述中的主语、谓语和宾语,当然没有 RDF 的模型表现的清楚,对比RDF的图来看,画出清晰的图模型还是非常重要的。在该模型里面有四个弧,所以有四个陈述。”anon:14df86:ecc3dee17b:-7fff” 是 Jena 内部生成的一个标识符,它不是一个URI,所以不能混为一谈,它只是 Jena 实例使用的一个简单内部标签。

上面结果的形式,万维网(W3C)的 RDF 核心工作小组(RDFCore Working Group)也定义过类似表现形式称为“N-Triples”。名字的意思是“三元表示法”,我们将会在后面的章节看到。

RDF的写入

Jena 有一系列的方法用来读写 RDF XML 文件。这些可以用来将一个 RDF 模型保存到一个文件中,之后可以再把它从文件中读回来。

上面的例子中创建了一个模型并且以三元组的格式输出。下面的例子输出的格式是RDF XML。这个代码非常简单:model.write 有一个 OutputStream 的参数。

// 声明变量
String personURI    = "http://somewhere/JohnSmith";
String givenName    = "John";
String familyName   = "Smith";
String fullName     = givenName + " " + familyName;

// 创建一个空的模型
Model model = ModelFactory.createDefaultModel();

// 创建资源
// 并且按照层叠式添加属性。
// model.createResource() 创建了一个空白节点
Resource johnSmith
  = model.createResource(personURI)
         .addProperty(VCARD.FN, fullName)
         .addProperty(VCARD.N,
                      model.createResource()
                           .addProperty(VCARD.Given, givenName)
                           .addProperty(VCARD.Family, familyName));

// 将模型以XML输出 model.write(System.out);

输出的结果应该是:

<rdf:RDF
  xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  xmlns:vcard='http://www.w3.org/2001/vcard-rdf/3.0#'
 >
  <rdf:Description rdf:about='http://somewhere/JohnSmith'>
    <vcard:FN>John Smith</vcard:FN>
    <vcard:N rdf:nodeID="A0"/>
  </rdf:Description>
  <rdf:Description rdf:nodeID="A0">
    <vcard:Given>John</vcard:Given>
    <vcard:Family>Smith</vcard:Family>
  </rdf:Description>
</rdf:RDF>

RDF 特别定义了如何将 RDF 表示为 XML 。RDF XML 格式是非常复杂。读者最好是去读一下 RDF 核心开发小组写的 Primer 以获取更完整的介绍。不过在这里我们快速的过一遍。

RDF 通常用<rdf:RDF>标签作为根节点。上面的 RDF 节点中定义了两个命名空间。

xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
xmlns:vcard='http://www.w3.org/2001/vcard-rdf/3.0#'

之后,有一个<rdf:Description>节点中,描述了一个URI 是”http://somewhere/JohnSmith”的资源。如果“rdf:about”属性不存在的话,那么这个节点将被表示成一个空白节点。

<vcard:FN>节点描述了资源的属性。属性名是在命名空间“vcard”中的本地名称“FN”。RDF可以将vcard转成对应的URI(在之前定义的命名空间里面),之后连接上本地名称生成一个完整的URI,”http://www.w3.org/2001/vcard-rdf/3.0#FN”,这个属性的值是一段文本“John Smith”。

<vcard:N>节点是一个资源。在这个例子中,这个资源通过对应的URI指向一个具体的资源,“A0”这个 URI 没有定义任何命名空间,所以“A0”指向的将是当前文件里面的一个资源。不过有没有发现,这个RDF XML其实有一个错误存在,我们创建的模型在这里并没一模一样的表示出来。因为在我们的模型里面是存在一个空白节点,但是这个节点却给了一个URI(A0),所以它将不再是一个空白节点。RDF/XML语法并不能很好的表示所有的RDF模型。

Jena有一个扩展接口允许将RDF写出到不同的格式中。上面使用的是一个标准的输出,Jena可是通过在write()里面添加参数来做其他RDF格式的输出。

model.write(System.out, "RDF/XML-ABBREV");

输出的结果如下:

<rdf:RDF
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#">
   <rdf:Description rdf:about="http://somewhere/JohnSmith">
     <vcard:N rdf:parseType="Resource">
       <vcard:Family>Smith</vcard:Family>
       <vcard:Given>John</vcard:Given>
     </vcard:N>
     <vcard:FN>John Smith</vcard:FN>
   </rdf:Description>
</rdf:RDF>

结果非常简约,甚至可以表现空白节点,但是由于他不适合写大型的模型,因为效率不能够被接受。所以写大型的文件并且保存空白节点,最好用N-Triples格式:

model.write(System.out, "N-TRIPLES");

结果就像上面的N-Triples给出的结果。

RDF的读取

下面的例子中,我们将会来读取所有RDF XML中的陈述并存入到一个模型中。这个例子,需要先将下面的文件存入本地的一个文件中,比如”example.rdf”

<rdf:RDF
  xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  xmlns:vcard='http://www.w3.org/2001/vcard-rdf/3.0#'
 >
  <rdf:Description rdf:nodeID="A0">
    <vcard:Family>Smith</vcard:Family>
    <vcard:Given>John</vcard:Given>
  </rdf:Description>
  <rdf:Description rdf:about='http://somewhere/JohnSmith/'>
    <vcard:FN>John Smith</vcard:FN>
    <vcard:N rdf:nodeID="A0"/>
  </rdf:Description>
  <rdf:Description rdf:about='http://somewhere/SarahJones/'>
    <vcard:FN>Sarah Jones</vcard:FN>
    <vcard:N rdf:nodeID="A1"/>
  </rdf:Description>
  <rdf:Description rdf:about='http://somewhere/MattJones/'>
    <vcard:FN>Matt Jones</vcard:FN>
    <vcard:N rdf:nodeID="A2"/>
  </rdf:Description>
  <rdf:Description rdf:nodeID="A3">
    <vcard:Family>Smith</vcard:Family>
    <vcard:Given>Rebecca</vcard:Given>
  </rdf:Description>
  <rdf:Description rdf:nodeID="A1">
    <vcard:Family>Jones</vcard:Family>
    <vcard:Given>Sarah</vcard:Given>
  </rdf:Description>
  <rdf:Description rdf:nodeID="A2">
    <vcard:Family>Jones</vcard:Family>
    <vcard:Given>Matthew</vcard:Given>
  </rdf:Description>
  <rdf:Description rdf:about='http://somewhere/RebeccaSmith/'>
    <vcard:FN>Becky Smith</vcard:FN>
    <vcard:N rdf:nodeID="A3"/>
  </rdf:Description>
</rdf:RDF>

下面的代码将会读取这个文件到一个空白模型中,之后再打印出来。Java程序需要和example.rdf存在同一个目录。

 // 创建一个空白的模型
 Model model = ModelFactory.createDefaultModel();

 // 使用文件管理器打开"example.rdf"
 InputStream in = FileManager.get().open( "example.rdf" );
if (in == null) {
    throw new IllegalArgumentException(
                                 "找不到文件: example.rdf");
}

// 读取RDF XML内容到模型中
model.read(in, null);

// 标准打印出来
model.write(System.out);

控制前缀

在代码中定义前缀

在前面的例子中,我们看到输出的XML文件声明了命名空间的前缀“vcard”,并使用前缀代替 URI。当 RDF 只是使用完整的 URIs,缩写的前缀是不存在的,Jena 提供了一种方式,使用 model.setNsPrefix() 方法建立前缀和 URI 映射,并输出。请看下面的例子。

 Model m = ModelFactory.createDefaultModel();
 String nsA = "http://somewhere/else#";
 String nsB = "http://nowhere/else#";
 Resource root = m.createResource( nsA + "root" );
 Property P = m.createProperty( nsA + "P" );
 Property Q = m.createProperty( nsB + "Q" );
 Resource x = m.createResource( nsA + "x" );
 Resource y = m.createResource( nsA + "y" );
 Resource z = m.createResource( nsA + "z" );
 m.add( root, P, x ).add( root, P, y ).add( y, Q, z );
 System.out.println( "# -- no special prefixes defined" );
 m.write( System.out );
 System.out.println( "# -- nsA defined" );
 // 建立前缀 nsA
 m.setNsPrefix( "nsA", nsA );
 m.write( System.out );
 System.out.println( "# -- nsA and cat defined" );
 //建立前缀 cat
 m.setNsPrefix( "cat", nsB );
 m.write( System.out );

下面输出三个不同的 RDF/XML,他们有不同的前缀映射。第一个是没有任何定义的前缀的。

# -- no special prefixes defined

<rdf:RDF
    xmlns:j.0="http://nowhere/else#"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:j.1="http://somewhere/else#" >
  <rdf:Description rdf:about="http://somewhere/else#root">
    <j.1:P rdf:resource="http://somewhere/else#x"/>
    <j.1:P rdf:resource="http://somewhere/else#y"/>
  </rdf:Description>
  <rdf:Description rdf:about="http://somewhere/else#y">
    <j.0:Q rdf:resource="http://somewhere/else#z"/>
  </rdf:Description>
</rdf:RDF>

我们看到”rdf”前缀的命名空间是自动声明的,因为这个在使用<rdf:RDF>和<rdf:resource>标签时是必须的,Jena 原生的支持了这种命名空间。对于属性“P”和“Q”,命名空间的声明应该是需要的,但是在这个例子中前缀并没有被引入,所以他们获得了一个自动生成地命名空间的前缀名字:j.o和j.1.

使用方法 setNsPrefix(String prefix, String URI) 来声明一个命名空间的 URI 的缩写“prefix”。Jena 需要 prefix 是一个合法的 XML 命名空间的名字,并且 URI 的结尾不能和命名空间名字重名。RDF/XML 格式将会把这些声明写入到命名空间的声明中并在输出的时候使用。

# -- nsA defined

<rdf:RDF
    xmlns:j.0="http://nowhere/else#"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:nsA="http://somewhere/else#" >
  <rdf:Description rdf:about="http://somewhere/else#root">
    <nsA:P rdf:resource="http://somewhere/else#x"/>
    <nsA:P rdf:resource="http://somewhere/else#y"/>
  </rdf:Description>
  <rdf:Description rdf:about="http://somewhere/else#y">
    <j.0:Q rdf:resource="http://somewhere/else#z"/>
  </rdf:Description>
</rdf:RDF>

上面这个输出,属性“P”的前缀名被声明为“nsA”,而“Q”没有。

# -- nsA and cat defined

<rdf:RDF
    xmlns:cat="http://nowhere/else#"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:nsA="http://somewhere/else#" >
  <rdf:Description rdf:about="http://somewhere/else#root">
    <nsA:P rdf:resource="http://somewhere/else#x"/>
    <nsA:P rdf:resource="http://somewhere/else#y"/>
  </rdf:Description>
  <rdf:Description rdf:about="http://somewhere/else#y">
    <cat:Q rdf:resource="http://somewhere/else#z"/>
  </rdf:Description>
</rdf:RDF>

这个结果中,两个前缀都被使用在了输出结果。

读取前缀名定义

上面说到前缀名的声明可以通过 setNsPrefix 实现。Jena 在使用 model.read() 的时候也会记住这些前缀的声明。把前面的结果保存到一个文件中,在运行下面的代码看看结果:

Model m2 = ModelFactory.createDefaultModel();
m2.read( "file:/tmp/fragment.rdf" );
m2.write( System.out );

你应该能看到输入文本中的前缀在输出结果中得到了保留,甚至包括还没有使用过的前缀。你可以使用 removeNsPrefix(String prefix) 来删除一些前缀,如果你不想在输出的时候显示出来。

N-Triples 格式没有任何简单的方式来写 URIs,因为它不需要任何具有缩写的输入也不做任何的带有缩写的输出。但是根据规则 N3 的定义,Jena 也支持缩写名的输入和输出。

Jena 对于前缀名的映射有更佳边界的操作以控制模型,例如从现存的映射中抽出一个 Java Map对象,或者直接增加一组完整的映射,请参考 PrefixMapping 的描述来获取详细信息。

Jena RDF 包

Jena 是一个 Java API 用来做语义网应用的。核心的 RDF 包是org.apache.jena.rdf.model。这个 API 被定义为一个 Interface(接口),所以任何应用程序的代码可以直接使用而不需要做任何的改变。这个包中的接口包括:模型(Model)、资源(Resource)、属性(Property)、文字(Literal)、陈述(Statement)和 RDF 中其他所有的关键理论,并且 ModelFactory 被用来创建模型。所以应用程序代码应该保持其独立的实现,最好是调用这些接口而不是使用特别的类。

org.apache.jena.tutorial 包中包含了该手册中所有例子的源代码。

org.apache.jena…impl 包中的实现类(implementation class)是一些常见的实现。比如,他们定义了类ResourvceImpl、PropertyImpl和LiteralImpl。

应用程序应该尽可能避免直接使用这些类,而是用createResource方法来代替 ResourceImpl 创建一个实例。这样的话,即使有一个 model 实例使用了一个优化的 Resource 实例,整个程序也不需要做任何的更新。

浏览一个模型

到目前为止,该手册还主要在说如何创建、读取和写入 RDF 模型。现在我们将会进入到控制模型的具体信息。

如果要找到一个资源,只需要给出该资源的 URI,在从模型里面使用 Model.getResource(String uri) 方法可以找到。这个方法被定义的运作形式是:如果 URI 的资源对象存在就会返回该对象,不然会创建一个。比如在上面的例子中,获取 John Smith 的资源值需要给出它的 URI:

Resource vcard = model.getResource(johnSmithURI)

这里返回了一个资源(Resource),资源的接口定义了一系列的方法来进入它的所有属性。使用 Resource.getProperty(Property p) 方法进入属性。该方法返回的结果是一个 Statement(陈述),这非按照通常Java的访问机制,返回一个Property。返回的这个 Statement 允许应用程序进入属性的值,使用一个返回陈述中的宾语的方法 getObject() 即可,例如返回前面例子中 vcard:N 属性的值:

// 强制转换返回类型为 Resource
Resource name = (Resource) vcard.getProperty(VCARD.N)
                                .getObject();

通常来说,一个陈述中的宾语的值可以是一个资源或者一段文字,返回的结果将是一个 Object(对象)。如果应用程序明确返回类型,可以在 Jena 中使用下面的方法来返回一个准确的对象类型,而不需要使用强制转换,下面的代码演示了如何操作:

// 返回 FN 属性的值为一个资源
Resource name = vcard.getProperty(VCARD.FN)
 .getResource();

相同的,返回一个属性的文字的值的方式:

// 返回 name 的值为一个文本
String fullName = vcard.getProperty(VCARD.FN)
                        .getString();

在上面例子中,将John Smith URI返回的资源定义为一个变量“vcard”,它只有vcard:FN 和 vcard:N 属性,RDF 运行机制允许一个资源可以有多个重复属性,但不同的的属性值; 比如,John Smith的另一个的昵称是“Adam”, 让我们给他添加一个。

// 添加两个昵称到vcard中
vcard.addProperty(VCARD.NICKNAME, "Smith")
     .addProperty(VCARD.NICKNAME, "Adman");

如之前所述, Jena 将 RDF 模型表示成一个陈述的集合, 所以添加一个已经存在的陈述(即属性和属性值相同的两个陈述)没有任何效果。 另外 Jena 并不会在刚刚定义的两个昵称中选择该返回哪一个,所以使用方法 vcard.getProperty(VCARD.NICKNAME) 的返回结果是不确定的。 Jena 会返回其中的一个结果,但是不能保证每次的请求都返回同一个值。

如果一种属性可能有多个,那么使用 Resource.listProperties(Property p) 方法能返回一个迭代器来列出所有的属性,这个迭代器中包括了查询属性的陈述,我们可以用下面的方法获取所有的昵称:

// 设置输出结果
System.out.println("The nicknames of \""
                      + fullName + "\" are:");
// 列出所有的昵称
StmtIterator iter = vcard.listProperties(VCARD.NICKNAME);
while (iter.hasNext()) {
    System.out.println("    " + iter.nextStatement()
                                    .getObject()
                                    .toString());
}

这个代码中的陈述迭代器 iter ,依次通过 nextStatement() 方法获取所有的对象,并将结果转换为一段字符串。这个代码返回的结果是:

The nicknames of "John Smith" are:
    Smithy
    Adman

PS. 结果列出这个John Smith 资源的所有属性,可以使用方法 listProperties(),不添加任何参数。

查询

前面的章节讲了在知道资源 URI 的情况下来访问模型。这节将会开始讲如何搜索模型。Jena 核心的 API 只提供一些有限的查询,更多的、更强大的 SPARQL 查询将在后面的章节中再描述。

Model.listStatements() 方法可以列出一个模型中的所有陈述,但是这也是一个很粗鲁的方式,所以不建议在大型模型中使用。Model.listSubjects() 也类似,返回全部带有属性的资源,只不过返回的是一个迭代器。

Model.listSubjectsWithProperty(Property p, RDFNode o) 方法将会返回一个属性是“p”、值是“o”的迭代器。如果确定在我们资源中只有vcard资源拥有vcard:FN属性,那么用下面的代码,我们可以找到所有的vcard的vcard:FN的资源:

// 列出 vcards
ResIterator iter = model.listSubjectsWithProperty(VCARD.FN);
while (iter.hasNext()) {
    Resource r = iter.nextResource();
    ...
}

所有这类的查询都是通过一个原始的查询方法:model.listStatements(Selector s)。这个方法返回模型中所有的陈述中具有”s”条件的迭代器。“Selector”选择器接口被设计为是一个可扩展的模式,但是目前只有一个实例,它是org.apache.jena.rdf.model包中的SimpleSelector。使用SimpleSelector是一个很罕见的情况,它使用的是一个特别的类而不是一个接口。SimpleSelector结构器需要三个参数:

Selector selector = new SimpleSelector(subject, predicate, object)

这个选择器选择的陈述需要满足给入的三个条件。如果使用“null”在其中任意位置,那么将会匹配该位置的任意值,下面的命令将会选择模型中所有的陈述

Selector selector = new SimpleSelector(null, null, null);

下面的命令将会选择所有带有VCARD.FN作为谓词的陈述。

Selector selector = new SimpleSelector(null, VCARD.FN, null);

另外简写的方式:

listStatements( S, P, O )

等于

listStatements( new SimpleSelector( S, P, O ) )

让我们对比一下使用和不使用选择器的两个代码的操作。下面代码是能够找到整个模型中所有带VCARD.FN属性的资源:

// 选择所有带有VCARD.FN属性的资源
ResIterator iter = model.listSubjectsWithProperty(VCARD.FN);
if (iter.hasNext()) {
    System.out.println("The database contains vcards for:");
    while (iter.hasNext()) {
        System.out.println("  " + iter.nextResource()
                                      .getProperty(VCARD.FN)
                                      .getString());
    }
} else {
    System.out.println("No vcards were found in the database");
}

输出结果应该是:

The database contains vcards for:
  Sarah Jones
  John Smith
  Matt Jones
  Becky Smith

另外一个操作是使用SimpleSelector代替listSubjectsWithProperty。让我们看一下如何实现更细腻的控制。SimpleSelector 选择所有带 VCARD.FN 的节点,并且这个作为选择器的 selects 方法来对结果进行过滤

// 选择所有带 VCARD.FN 属性的资源
// 并且值结束于“Smith”
StmtIterator iter = model.listStatements(
    new SimpleSelector(null, VCARD.FN, (RDFNode) null) {
        public boolean selects(Statement s)
            {return s.getString().endsWith("Smith");}
    });

这个示例中的代码使用Java内嵌机制重构了该类。 这里的方法 selects() 检查并确保了全名必须结束于“Smith”。需要指出这里的匹配是基于陈述进行过滤的。

The database contains vcards for:
  John Smith
  Becky Smith

也许你会想

// 使用所有selects方法中的过滤
StmtIterator iter = model.listStatements(
  new SimpleSelector(null, null, (RDFNode) null) {
    public boolean selects(Statement s) {
      return (subject == null   || s.getSubject().equals(subject))
          && (predicate == null || s.getPredicate().equals(predicate))
          && (object == null    || s.getObject().equals(object)) ;
          }
     }
     });

上面这个等于

StmtIterator iter =
  model.listStatements(new SimpleSelector(subject, predicate, object)

操作模型

Jena提供了三种方式来操作模型,分别是:并集、交集和补集。

两个模型的合并是每个模型中的陈述的集合的并集。这是RDF所支持的一个关键的操作。它使得离散的数据源合并到一块。请看下面的两个模型:

figure 4figure 5

当他们合并的时候,两个http://…JohnSmith节点合并到一个,并且重复的vcard:FN弧被丢弃,最后生成:

figure 6

使用下面的代码看看结果是什么:

// 读取 RDF/XML 文件
model1.read(new InputStreamReader(in1), "");
model2.read(new InputStreamReader(in2), "");

// 合并模型
Model model = model1.union(model2);

// 把模型按RDF/XML输出
model.write(system.out, "RDF/XML-ABBREV");

输出的结果是:

<rdf:RDF
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#">
  <rdf:Description rdf:about="http://somewhere/JohnSmith/">
    <vcard:EMAIL>
      <vcard:internet>
        <rdf:value>John@somewhere.com</rdf:value>
      </vcard:internet>
    </vcard:EMAIL>
    <vcard:N rdf:parseType="Resource">
      <vcard:Given>John</vcard:Given>
      <vcard:Family>Smith</vcard:Family>
    </vcard:N>
    <vcard:FN>John Smith</vcard:FN>
  </rdf:Description>
</rdf:RDF>

即使你不熟悉RDF/XML语法的具体格式,也应该很容易的看出两个被很好的合并到一起。交集和补集的操作也相似的,使用方法.intersection(Model)和.difference(Model)。

容器

RDF定义了一些特殊的资源来表示所有东西的集合。这些资源称为“容器(Containers)”。容器的成员既可以是文本也可以是资源。他们有三种类型:

  • 一个BAG,无序的集合;
  • 一个ALT,无序的集合同时是具有替代性的,例如二选一的概念;
  • 一个SEG,有序的集合。

一个容器可以表示为一个资源。这个资源应该有一个rdf:type属性,它的值应该是rdf:Bag、rdf:Alt 或 rdf:Seq,再或者是他们中的一个子类,根据具体的类别而定。容器的第一个成员是一个容器属性rdf:_1的值;第二个是容器属性rdf:_2的值,以此类推。

例如,下面的模型是一个包含了vcards的Smith的Bag容器:

figure 3

虽然bag容器的成员中的属性有rdf:_1, rdf:_2等等,但是其序列是没有被指出的。我们可以替换两个属性的值,模型表示的信息将会是一样的。

Alt容器是为了表示替代的概念。比如,我们有一个资源表示了一个软件产品。它可以有一个属性指出他从哪里来。而这个属性的值应该是Alt集合包含了不同的网站可以用来下载这个软件。Alt是一个无序的,除了rdf:_1属性比较特殊,他表示默认的选择。

虽然容器可以通过资源或者属性的基本机制来控制,Jena还是提供了接口和实例类用来控制它们。

// 创建一个bag容器
Bag smiths = model.createBag();

// 选择所有带 VCARD.FN 属性的资源
// 其值结束于 "Smith"
StmtIterator iter = model.listStatements(
    new SimpleSelector(null, VCARD.FN, (RDFNode) null) {
        public boolean selects(Statement s) {
                return s.getString().endsWith("Smith");
        }
    });
// 增加 Smith's 到容器中
while (iter.hasNext()) {
    smiths.add(iter.nextStatement().getSubject());
}

如果输出模型,他将会包含下面这些内容表示一个Bag容器的资源:

<rdf:RDF
  xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  xmlns:vcard='http://www.w3.org/2001/vcard-rdf/3.0#'
 >
...
  <rdf:Description rdf:nodeID="A3">
    <rdf:type rdf:resource='http://www.w3.org/1999/02/22-rdf-syntax-ns#Bag'/>
    <rdf:_1 rdf:resource='http://somewhere/JohnSmith/'/>
    <rdf:_2 rdf:resource='http://somewhere/RebeccaSmith/'/>
  </rdf:Description>
</rdf:RDF>

容器的接口提供了一个迭代器来列出容器中的内容:

// 输出bag容器中的成员
NodeIterator iter2 = smiths.iterator();
if (iter2.hasNext()) {
    System.out.println("The bag contains:");
    while (iter2.hasNext()) {
        System.out.println("  " +
            ((Resource) iter2.next())
                            .getProperty(VCARD.FN)
                            .getString());
    }
} else {
    System.out.println("The bag is empty");
}

结果如下:

The bag contains:
  John Smith
  Becky Smith

Jena类中提供了控制容器的方法,包括:增加一个新成员、插入一个新成员到容器的中间并且移除一个现有的成员。Jena容器类目前能确保列出有序列的属性从rdf:_1开始。RDF核心工作小组放松了一些限制,允许表示容器中的一部分属性。

关于“文本Literals”和“数据类型Datatypes”

RDF literals(文本)不是一个简单的字符串。 Literals应该有语言标签指出该文本的语言。如文本“chat”有英语标签,所有它不应该被认为是法语中的“chat”。

如果有这样的两种文本。一种是文本本身,第二种是带有XML标签的文本。当RDF模型把它写成RDF/XML文件时,一个parseType=’Literal’的属性将会添加到结果中来指出区别。

在Jena中,创建一个Literal:

// 创建一个资源
Resource r = model.createResource();

// 添加属性
r.addProperty(RDFS.label, model.createLiteral("chat", "en"))
 .addProperty(RDFS.label, model.createLiteral("chat", "fr"))
 .addProperty(RDFS.label, model.createLiteral("<em>chat</em>", true));

// 写出模型
model.write(system.out);

其产生:

<rdf:RDF
  xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  xmlns:rdfs='http://www.w3.org/2000/01/rdf-schema#'
 >
  <rdf:Description rdf:nodeID="A0">
    <rdfs:label xml:lang='en'>chat</rdfs:label>
    <rdfs:label xml:lang='fr'>chat</rdfs:label>
    <rdfs:label rdf:parseType='Literal'><em>chat</em></rdfs:label>
  </rdf:Description>
</rdf:RDF>

我们对于这两个相同的literals(文本)定义,他们应该是一样的,要么是XML的文本,要么就是简单的文本。另外,他们不应该有不同的语言标签,语言标签应该是相同的。

Jena的接口同样提供了literal的类别。一个比较老的方法是把 literal 当成一个字符的缩写方法,如果创建简单的literals,我们可以忽略 model.createLiteral(),literals 是 Java 中的字符串类别:

// 创建一个资源
Resource r = model.createResource();

// 增加一个属性
r.addProperty(RDFS.label, "11")
 .addProperty(RDFS.label, 11);

// 输出结果
model.write(system.out, "N-TRIPLE");

输出结果将是:

_:A... <http://www.w3.org/2000/01/rdf-schema#label> "11" .

尽管添加了两个“11”,但是只有一个陈述

RDF核心工作小组定义了在RDF中支持数据类别的机制,Jena同时也支持这些类型的literal机制,在这个教程里面将不会提到。

在这章里,我主要围绕 Jena RDF 核心 API 做了大量的说明,包括读写,命名空间,查询,容易等等,在后面一章,我将开始讲强大的 SPARQL 查询。

转摘请标明出处: http://www.flykun.com

 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.