RDF 和 Jena RDF API 简介(RDF core API tutorial)

1 篇文章 0 订阅

RDF 和 Jena RDF API 简介

前言

代码中的官方RDF数据下载地址:https://jena.apache.org/tutorials/sparql_data/
官方教程的网址:https://jena.apache.org/tutorials/rdf_api.html
本文章是对官方文档的翻译,在此基础上增加了一些自己的理解。

一、问题描述

  这是对W3C的资源描述框架(RDF)和Jena(RDF的Java API)的教程介绍。它是为那些不熟悉RDF的程序员编写的,他们通过原型设计学习得最好,或者出于其他原因,希望快速进入实现阶段。假设对XML和Java都有一定的熟悉程度。

  在没有先理解RDF数据模型的情况下过快地进行实施,往往会导致挫败和失望。然而,仅仅研究数据模型本身是枯燥的,而且常常会陷入费解的形而上学难题中。更好的方法是并行地理解数据模型以及如何使用它。先学习一点数据模型并尝试使用。然后再学习一点并尝试应用。这样,理论可以指导实践,实践又可以反过来验证理论。数据模型相当简单,因此这种方法并不费时。

  RDF具有XML语法,许多熟悉XML的人会以该语法来思考RDF。这是一个错误。应该从数据模型的角度来理解RDF。RDF数据可以用XML表示,但理解语法比理解数据模型更次要。
  Jena API的实现,包括本教程中使用的所有示例的工作源代码,可以从Jena.apache.org/download/index.cgi下载。

二、介绍

  资源描述框架(RDF)是用于描述资源的标准(从技术上讲,它是W3C的推荐标准)。什么是资源?这是一个相当深刻的问题,精确的定义仍然存在争议。就我们的目的而言,我们可以将其视为任何我们能识别的东西。你是一个资源,你的主页也是一个资源,这个教程是一个资源,数字1也是一个资源,《白鲸记》中的大白鲸也是一个资源。

  本教程中的示例将是关于人的。它们使用VCARDS的RDF表示。RDF最好以节点图和弧形图的形式来考虑。一个简单的vcard在RDF中可能是这样的:
在这里插入图片描述
  John Smith 资源显示为一个椭圆,由统一资源标识符 (URI) 标识,本例中为 “http://…/JohnSmith”。如果您尝试使用浏览器访问该资源,您不可能成功;四月的第一个笑话不提也罢,如果您的浏览器能够将John Smith传送到您的桌面上,您会感到非常惊讶。如果您不熟悉URI,请将其简单地理解为看起来相当奇怪的名称。

  资源具有属性。在这些示例中,我们感兴趣的是 John Smith 名片上的属性。图 1 只显示了一个属性,即 John Smith 的全名。属性用弧线表示,弧线上标有属性的名称。属性名也是一个 URI,但由于 URI 较长而且繁琐,所以图中以 XML qname 的形式表示。在’:‘之前的部分称为命名空间前缀,代表一个命名空间。在’:'之后的部分称为本地名称,表示该名称空间中的名称。当以RDF XML编写时,属性通常以这种qname形式表示,这是在图表和文本中表示属性的一种方便的速记方法。然而,严格来说,属性是由 URI 标识的。nsprefix:localname 形式是名称空间的 URI 与 localname 连接的简写。不要求属性的 URI 在浏览器访问时解析到任何内容。

  每个属性都有一个值。在这种情况下,值是文字,目前我们可以将其视为字符串2。文字以矩形显示。

  Jena是一个Java API,可用于创建和操作像这样的RDF图。Jena有对象类来表示图、资源、属性和文字。代表资源、属性和文字的接口分别称为资源(Resource)、属性(Property)和文字(Literal)。在Jena中,图被称为模型,由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或model。Jena包含Model接口的其他实现,例如使用关系数据库的实现:这些类型的Model也可以从ModelFactory.ModelFactorycreateDefaultModel()中获得。

  然后创建John Smith资源并向其添加属性。该属性由一个“常量”类VCARD提供,它保存表示VCARD 模式中所有定义的对象。Jena为其他众所周知的模式提供常量类,例如RDF和RDF模式本身、Dublin Core和OWL。
创建资源和添加属性的代码可以以级联式编写得更紧凑:

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

  现在让我们为vcard添加更多细节,探索RDF和Jena的更多特性。
  在第一个示例中,属性值是一个文字。RDF属性也可以将其他资源作为其值。本例使用了一种常见的RDF技术,展示了如何表示John Smith名字的不同部分:

在这里插入图片描述  这里我们添加了一个新属性 vcard:N,用于表示 John Smith 的姓名结构。该模型有几个值得注意的地方。请注意,vcard:N属性将资源作为其值。还请注意,表示复合名称的椭圆没有 URI。它被称为空白节点。

  构造这个例子的Jena代码同样非常简单。首先是一些声明和空模型的创建。

// some definitions
String personURI    = "http://somewhere/JohnSmith";
String givenName    = "John";
String familyName   = "Smith";
String fullName     = givenName + " " + familyName;

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

// 创建资源
//   并添加属性级联样式
Resource johnSmith
  = model.createResource(personURI)
         .addProperty(VCARD.FN, fullName)
         .addProperty(VCARD.N,
                      model.createResource()
                           .addProperty(VCARD.Given, givenName)
                           .addProperty(VCARD.Family, familyName));

  这个例子的工作代码可以在Jena发行版的 /src examples 目录中找到教程2。

三、语句

  RDF模型中的每一条弧都称为语句。每条语句都断言关于资源的一个事实。一条语句有三个部分:

  • 主语是弧离开的资源
  • 谓词是标示弧的属性
  • 宾语是弧所指向的资源或文字
    一个语句有时被称为三元组,因为它有三个部分。

  RDF 模型表示为一组语句。每调用一次 tutorial2,就向模型添加一条语句。(因为模型是一组语句,所以添加重复的语句不会有任何影响)。Jena模型接口定义了一个方法,它返回 Java 在模型中所有语句的子类型。它有一个从迭代器返回下一条语句的方法(与将传递的语句相同,已经被转换 )。该接口为语句的主语、谓语和宾语提供了访问方法。

addPropertylistStatements()StmtIteratorIteratorStmtIteratornextStatement()next()StatementStatement

  现在,我们将使用该接口扩展tutorial2,以列出所有创建的语句并打印出来。完整的代码可以在教程3中找到。

// 列出模型中的语句
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()RDFNodeResourceLiteralinstanceof

运行时,此程序应生成类似于以下内容的输出:

http://somewhere/JohnSmith http://www.w3.org/2001/vcard-rdf/3.0#N 413f6415-c3b0-4259-b74d-4bd6e757eb60 .
413f6415-c3b0-4259-b74d-4bd6e757eb60 http://www.w3.org/2001/vcard-rdf/3.0#Family  "Smith" .
413f6415-c3b0-4259-b74d-4bd6e757eb60 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" .

  现在您知道为什么用它画模型更清楚了吧。如果您仔细观察,就会发现每行由三个字段组成,分别代表每个语句的主语、谓语和宾语。模型中有四条弧线,因此有四条语句。14df86:ecc3dee17b:-7fff "是Jena生成的内部标识符。它不是 URI,不应与 URI 混淆。它只是Jena实现中使用的一个内部标签。

  W3C RDFCore工作组定义了一种类似的简单符号叫做N-Triples。这个名称意为“三元组符号”。我们将在下一节看到Jena内置了N-Triples写入器。

四、编写RDF

  Jena有将RDF读写为XML的方法。这些可以用于将RDF模型保存到文件中,然后再将其读回来。
  教程3创建了一个模型并以三元组形式写出。教程4修改了教程3,将RDF XML形式的模型写入标准输出流。代码再次非常简单:可以采用参数:.model.writeOutputStream

// 现在将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语法相当复杂。读者可以参考RDFCore工作组正在开发的引物进行更详细的介绍。然而,让我们快速了解一下如何解释上述内容。

  RDF 通常嵌入在 <rdf:RDF> 元素中。如果有其他方法可以知道某些 XML 是 RDF,那么该元素是可选的,但它通常是存在的。RDF元素定义了文档中使用的两个命名空间。然后是 <rdf:Description> 元素,它描述了 URI 为 "http://somewhere/JohnSmith "的资源。如果缺少 rdf:about 属性,该元素将表示一个空白节点。

  <vcard:FN> 元素描述资源的一个属性。属性名称是 vcard 命名空间中的 “FN”。RDF 通过连接命名空间前缀的 URI 引用和名称的本地名称部分 “FN”,将其转换为 URI 引用。这样就得到了一个 URI 引用:“http://www.w3.org/2001/vcard-rdf/3.0#FN”。该属性的值是文字 “John Smith”。

  <vcard:FN>元素是一种资源。在这种情况下,资源由相对URI引用表示。RDF通过将其与当前文档的基本URI连接,将其转换为绝对URI引用。

  这个RDF XML存在一个错误,它并没有准确地表示我们创建的模型。模型中的空白节点被赋予了一个URI引用,因此它不再是空白节点。RDF/XML语法不能表示所有的RDF模型,例如不能表示作为两个语句对象的空白节点。我们使用的“dumb”的写入器并没有尝试正确地写入可以正确写入的模型的子集。它给每个空白节点都赋予了一个URI,使它不再是空白节点。

  Jena有一个可扩展的接口,允许为不同的RDF序列化语言轻松插入新的 writers 。上述调用调用了标准的 "dumb"写入器(writer)。Jena还包含一个更复杂的RDF/XML写入器,可以通过函数调用:RDFDataMgr.write 调用。

// now write the model in a pretty form
RDFDataMgr.write(System.out, model, Lang.RDFXML);

  这种 writer,即所谓的 PrettyWriter,利用了RDF/XML缩写语法的特点来更紧凑地写入Model。它还能够在可能的情况下保留空白节点。然而,它并不适合编写非常大的 Model,因为其性能不太可能被接受。要编写大型文件并保留空白节点,请使用N-Triples 格式编写:

// now write the model in N-TRIPLES form
RDFDataMgr.write(System.out, model, Lang.NTRIPLES);

  这将产生类似于教程3的输出,它符合N-Triples规范。

五、读取RDF

  教程5演示了如何将以RDF XML形式记录的语句读取到模型中。在本教程中,我们提供了一个小型的RDF/XML形式的vcards数据库。下面的代码将把它读入并写出。请注意,要运行该应用程序,输入文件必须在当前目录下。

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

// 使用 RDFDataMgr 查找输入文件
InputStream in = RDFDataMgr.open( inputFileName );
if (in == null) {
    throw new IllegalArgumentException("File: " + inputFileName + " not found");
}

// 读取 RDF/XML 文件
model.read(in, null);

// 按照标准写出它
model.write(System.out);

  read() 方法调用的第二个参数是用于解析相对 URI 的 URI。由于测试文件中没有相对 URI 引用,因此允许为空。运行时,教程 5 将产生如下 XML 输出:

<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>

六、控制前缀(明确的前缀定义)

  在上一节中,我们看到输出 XML 声明了命名空间前缀 vcard,并使用该前缀来缩写 URI。虽然 RDF 只使用完整的 URI,而不使用这种缩写形式,但 Jena 提供了通过前缀映射来控制输出中使用的命名空间的方法。下面是一个简单的例子。

public static void controlPrefix() {
        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( "# -- 未定义特殊前缀 --" );
        m.write( System.out );
        System.out.println( "# -- 前缀定义为 nsA --" );
        m.setNsPrefix( "nsA", nsA );
        m.write( System.out );
        System.out.println( "# -- 前缀定义为 nsA 和 cat,这里将字符串 nsB 对应的前缀改为了cat --" );
        m.setNsPrefix( "cat", nsB );
        m.write( System.out );
    }

  这个片段的输出是三个RDF/XML,有三个不同的前缀映射。首先是默认值,除了标准前缀之外没有其他前缀:

# -- 未定义特殊前缀

<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 等标记所必需的。在使用 P 和 Q 这两个属性时也需要声明 XML 名称空间,但由于在本例中模型没有引入它们的前缀,因此它们获得了自创的名称空间名称:j.0 和 j.1。

  方法setNsPrefix(字符串前缀,字符串URI)声明命名空间URI可以通过前缀缩写。Jena要求前缀是合法的XML命名空间名称,并且URI以非名称字符结尾(在XML中,名称字符是指字母、数字和一些特殊字符,例如下划线、连字符和点号)。RDF/XML编写器将把这些前缀声明转换为XML命名空间声明,并在其输出中使用它们:

# -- 前缀定义为 nsA --
<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>

  另一个命名空间仍然获得构造的默认名称,但nsA名称现在用于属性标记中。前缀名称不需要与Jena代码中的变量有任何关系,前缀的名称可以不与变量名称相同。只要确保前缀是合法的XML命名空间名称,并且使用这个前缀声明的URI以非名称字符结尾,就可以在RDF/XML输出中正确地使用这个前缀:

# -- 前缀定义为 nsA 和 cat,这里将字符串 nsB 对应的前缀改为了cat --
<rdf:RDF
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:nsA="http://somewhere/else#"
    xmlns:cat="http://nowhere/else#">
  <rdf:Description rdf:about="http://somewhere/else#root">
    <nsA:P>
      <rdf:Description rdf:about="http://somewhere/else#y">
        <cat:Q rdf:resource="http://somewhere/else#z"/>
      </rdf:Description>
    </nsA:P>
    <nsA:P rdf:resource="http://somewhere/else#x"/>
  </rdf:Description>
</rdf:RDF>

  两个前缀都用于输出,无需生成前缀。

七、隐含前缀定义

  除了调用setNsPrefix提供的前缀声明外,Jena还将记住在model.read()的输入中使用的前缀。将前面片段的输出结果粘贴到某个文件中,URL 文件为:/tmp/fragment.rdf。然后运行代码:

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

  你会发现,输出中保留了输入中的前缀。所有前缀都会被写入,即使它们没有在任何地方被使用。如果不想在输出中使用前缀,可以使用 removeNsPrefix(String prefix) 删除前缀,代码如下。

public static void deletImplicitPrefix(){
			 // 创建模型
        Model model1 = ModelFactory.createDefaultModel();
        // 添加前缀声明
        model1.setNsPrefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
        model1.setNsPrefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#");
        System.out.println("----------没删除之前------------");
        /*使用Jena API中的getNsPrefixMap()方法获取Jena模型中定义的所有前缀声明的映射。
		该方法返回一个Map<String, String>对象,其中包含模型中所有前缀声明的键值对,
		其中键是前缀名称,值是前缀所对应的命名空间URI。*/ 
        Map<String, String> prefixes1 = model1.getNsPrefixMap();
        for (Map.Entry<String, String> entry : prefixes1.entrySet()) {
            System.out.println("Prefix: " + entry.getKey() + ", Namespace: " + entry.getValue());
        }
        System.out.println("---------把定义为rdf的前缀删除之后-------------");
        // 删除前缀声明
        model1.removeNsPrefix("rdf");
        // 输出模型中的所有前缀声明
        //这里重新接受模型的前缀,否则输出结果会一样
        Map<String, String> prefixes = model1.getNsPrefixMap();
        for (Map.Entry<String, String> entry : prefixes.entrySet()) {
            System.out.println("Prefix: " + entry.getKey() + ", Namespace: " + entry.getValue());
        }
    }

操作结果:

在这里插入图片描述

  由于NTriples没有任何简写URI的方式,因此它在输出时不考虑前缀,也不在输入时提供任何前缀。而N3表示法支持使用短缩写前缀名称,并且在输入时记录它们,并在输出时使用它们。Jena也支持N3表示法。

  Jena还有一些操作,可以对模型保存的前缀映射进行进一步操作,例如提取一个Java映射的现有映射,或一次性添加整个映射组;有关详细信息,请参阅PrefixMapping的文档。

八、Jena RDF包

  Jena 是用于语义网络应用程序的 Java API。应用程序开发人员的关键 RDF 包是 org.apache.jena.rdf.model。该 API 是以接口的形式定义的,因此应用程序代码无需更改即可使用不同的实现。该软件包包含用于表示模型、资源、属性、字面量、语句和 RDF 所有其他关键概念的接口,以及用于创建模型的 ModelFactory。为了使应用程序代码不受实现的影响,最好尽可能使用接口,而不是特定类的实现。

  org.apache.jena.tutorial 包含有本教程中使用的所有示例的工作源代码。

  org.apache.jena…impl 包包含的实现类可能是许多实现所共有的。例如,它们定义了 ResourceImpl、PropertyImpl 和 LiteralImpl 等类,这些类可以直接使用,也可以被不同的子类实现。应用程序很少(如果有的话)直接使用这些类。例如,与其创建 ResourceImpl 的新实例,不如使用正在使用的模型的 createResource 方法。这样,如果模型实现使用了 Resource 的优化实现,就无需在两种类型之间进行转换。

九、遍历模型

  到目前为止,本教程主要涉及 RDF 模型的创建、读取和写入。现在是访问模型中信息的时候了。

  给定资源的 URI 后,可以使用 Model.getResource(String uri) 方法从模型中检索资源对象。该方法的定义是,如果模型中存在资源对象,则返回该资源对象,否则创建一个新的资源对象。例如,从教程 5 中的文件读入的模型中检索 John Smith 资源:

public static void navigatingModel (Model model) {
	// 从模型中检索John Smith的 vcard 资源
	String johnSmithURI = "http://somewhere/JohnSmith/";
	Resource vcard = model.getResource(johnSmithURI);
	System.out.println(vcard);

	//一个Jena API方法,用于获取资源对象"vcard"的所有属性以及对应的值。它返回一个StmtIterator(Statement Iterator)对象,用于遍历资源的属性列表。
	//一个Statement对象由三个部分组成:主语(subject)、谓语(predicate)和宾语(object),在这里,"vcard"是主语,属性是谓语,属性值是宾语。
	StmtIterator iter = vcard.listProperties();
    while (iter.hasNext()) {
        Statement stmt = iter.nextStatement();
        //获取当前属性的谓语URI(属性的URI)。谓语URI是一个唯一标识属性的字符串。
        String predicateURI = stmt.getPredicate().getURI();
        //获取当前属性的宾语的字符串表示。属性的宾语是属性的值,可以是字符串、整数、浮点数或其他RDF节点。
        String objectValue = stmt.getObject().toString();
        // 处理属性和值
        System.out.println(predicateURI);
        System.out.println(objectValue);
        }
    }

rdf 文件有关 JohnSmith 的内容:

 <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:Family>Smith</vcard:Family>
    <vcard:Given>John</vcard:Given>
  </rdf:Description>

输出结果:
在这里插入图片描述

  Resource 接口定义了访问资源属性的一些方法。Resource.getProperty(Property p) 方法用于访问资源的属性。这个方法不遵循通常的Java访问器约定,因为返回的对象类型是 Statement,而不是您可能期望的 Property。返回整个语句允许应用程序使用其中一个访问器方法来访问属性的值,该方法返回语句的对象。例如,使用上面的John Smith的资源,检索其作为 vcard:FN 属性值的资源,可以使用以下代码:

    public static void retrieveProperty(Model model) {
        String johnSmithURI = "http://somewhere/JohnSmith/";
        // 获取资源
        Resource vcard = model.getResource(johnSmithURI);
        // 获取 vcard:FN 属性的值
        Statement stmt = vcard.getProperty(VCARD.FN);
        RDFNode obj = stmt.getObject();
        System.out.println("vcard:N value: " + obj.toString());


        System.out.println("---------以下是官方的示例-------------");
		Resource name = (Resource) vcard.getProperty(VCARD.N)
                                .getObject();
        System.out.println("vcard:N value: " + name.toString());
                                
    }

输出结果:

在这里插入图片描述

  同样,也可以检索属性的字面值:

    public static void retrievePropertyLiteral(Model model) {
        String johnSmithURI = "http://somewhere/JohnSmith/";
        // 获取资源
        Resource vcard = model.getResource(johnSmithURI);
        String fullName = vcard.getProperty(VCARD.FN)
                .getString();
        System.out.println(fullName);
    }

在这里插入图片描述

  在本例中,vcard 资源只有一个 vcard:FN 属性和一个 vcard:N 属性。RDF 允许资源重复一个属性;例如,亚当可能有不止一个昵称。让我们给他取两个:

    public static void addNickname(Model model) {
        String johnSmithURI = "http://somewhere/JohnSmith/";
        // 获取资源
        Resource vcard = model.getResource(johnSmithURI);
        vcard.addProperty(VCARD.NICKNAME, "Smithy")
                .addProperty(VCARD.NICKNAME, "Adman");
        StmtIterator iter = vcard.listProperties(VCARD.NICKNAME);
        while (iter.hasNext()) {
            Statement stmt = iter.nextStatement();
            RDFNode obj = stmt.getObject();
            System.out.println("vcard:NICKNAME value: " + obj.toString());
        }
    }

运行结果:

在这里插入图片描述
将添加的昵称写入到RDF文件:

String inputFileName  = "java01/src/main/java/come/jena/rdf/vc-db-1.rdf";
FileOutputStream out = null; 
try {
	out = new FileOutputStream(inputFileName);
} 
catch (FileNotFoundException e) {
	e.printStackTrace();
	}
model.write(out, "RDF/XML-ABBREV");
out.close();

运行结果:

在这里插入图片描述

  如前所述,Jena 将 RDF 模型表示为一组语句,因此添加一个主语、谓语和宾语都已经在模型中的语句将不会产生影响。Jena没有定义在模型中的两个昵称的哪一个将被返回。调用vcard.getProperty(vcard.NICKNAME)的结果是不确定的。Jena将返回其中一个值,但即使连续两次调用也不能保证返回相同的值。

  如果一个属性可能出现多次,那么可以使用 Resource.listProperties(Property p) 方法返回一个迭代器,以列出所有该属性的值。该方法返回一个迭代器,该迭代器返回类型为 Statement 的对象。我们可以像这样列出昵称:

// set up the output
System.out.println("The nicknames of \""
                      + fullName + "\" are:");
// list the nicknames
StmtIterator iter = vcard.listProperties(VCARD.NICKNAME);
while (iter.hasNext()) {
    System.out.println("    " + iter.nextStatement()
                                    .getObject()
                                    .toString());
}

  这段代码可以在教程 6 中找到。语句迭代器 iter 会生成主语为 vcard、谓语为 VCARD.NICKNAME 的每一条语句,因此循环使用 nextStatement() 可以获取每一条语句,获取对象字段并将其转换为字符串。代码运行时会产生以下输出:

The nicknames of "John Smith" are:
    Smithy
    Adman

  使用不带参数的 listProperties() 方法可以列出资源的所有属性。

十、查询模型

  上一节讨论了从已知 URI 资源遍历模型的情况。本节将讨论搜索模型。核心的 Jena API 只支持有限的查询功能。SPARQL 更强大的查询功能将在其他地方介绍。

  Model.listStatements() 方法会列出模型中的所有语句,这也许是查询模型的最简单方法。但是,不建议在非常大的模型中使用该方法,因为它会将整个模型加载到内存中,可能会导致内存不足或性能问题。Model.listSubjects() 方法与之类似,返回的迭代器包含所有作为语句主语的资源。换句话说,该迭代器遍历的是所有具有属性的资源,这些资源是作为语句主语出现的。这个方法可以用于查找具有特定属性的所有资源,而无需加载整个模型到内存中。

  Model.listSubjectsWithProperty(Property p, RDFNode o) 将返回具有属性 p 且值为 o 的所有资源的迭代器。如果我们假设只有 vcard 资源才具有 vcard:FN 属性,而在我们的数据中,所有此类资源都具有这样的属性,那么我们可以像这样找到所有的 vcard。参数p 是一个 Property 对象,它指定要查找的属性。例如,在以下代码中,我们使用 VCARD.FN 属性来查找具有 vcard:FN 属性的资源;参数o 是一个 RDFNode 对象,它指定要查找的属性值。该方法也可以只给一个属性参数,将返回所有具有该属性的资源。例如,在以下代码中,我们查找的是具有 vcard:FN 属性且该属性值为 “John Smith” 的资源:

// list vcards
ResIterator iter = model.listSubjectsWithProperty(VCARD.FN,"John Smith");
while (iter.hasNext()) {
    Resource r = iter.nextResource();
    System.out.println(r.toString());
}

在RDF文件中,具有属性FN且其值为"John Smith"的有两个,如下所示:

  <rdf:Description rdf:about="http://somewhere/SarahJones/">
    <vcard:N rdf:parseType="Resource">
      <vcard:Given>Sarah</vcard:Given>
      <vcard:Family>Jones</vcard:Family>
    </vcard:N>
    <vcard:FN>Sarah Jones</vcard:FN>
    <vcard:FN>John Smith</vcard:FN>
  </rdf:Description>
  <rdf:Description rdf:about="http://somewhere/JohnSmith/">
    <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>

代码的运行结果如下:

在这里插入图片描述

  所有这些查询方法只是基本查询方法 model.listStatements(Selector s) 的语法糖。Selector 对象 s 决定了要选择哪些语句,Model.listStatements(Selector s) 方法会返回一个迭代器,用于遍历这些被选择的语句。Selector 接口被设计成是可扩展的,然而,目前只有一个实现它的类,即存在于 org.apache.jena.rdf.model 包中的 SimpleSelector 类。在 Jena 中使用 SimpleSelector 是一种罕见的情况,需要使用特定的类而不是接口。SimpleSelector 构造函数包含三个参数:

  • subject:表示要选择的语句的主语。可以传递一个 Resource 对象或者是 null,表示选择所有主语。
  • predicate:表示要选择的语句的谓语。可以传递一个 Property 对象或者是 null,表示选择所有谓语。
  • object:表示要选择的语句的宾语。可以传递一个 RDFNode 对象或者是 null,表示选择所有宾语。
Selector selector = new SimpleSelector(subject, predicate, object);

  这个selector 将选择所有具有与 subject 匹配的主语、与 predicate 匹配的谓语以及与 object 匹配的宾语的语句。如果在任何位置提供了 null,则匹配任何内容;否则,它们将匹配相应的相等资源或字面量。如果两个资源具有相等的 URI 或者是相同的空白节点,则它们是相等的;如果两个字面量的所有组成部分都相等,则它们是相等的。

  在RDF(Resource Description Framework)中,每个资源都有一个唯一的标识符,称为 URI(Uniform Resource Identifier)。如果两个资源具有相同的 URI,则它们被视为相等的。此外,如果两个资源都是空白节点,则它们也被视为相等的。空白节点是没有 URI 的节点,它们只是用于在 RDF 图中连接其他节点的中间节点。

  另一方面,字面量是表示值的节点,例如字符串、数字或日期。如果两个字面量的所有组件都相等,则它们被视为相等的。例如,如果两个字面量都是字符串 “hello”,则它们被视为相等的。但是,如果一个字面量是字符串 “hello”,另一个字面量是整数 5,则它们不相等,因为它们的类型不同。 因此,下面的代码将选择模型中的所有语句。

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 ) )

  下面的代码可以在教程7中找到,它列出了数据库中所有vcard的全名。

// select all the resources with a VCARD.FN property
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");
}

输出结果应类似于下面的内容:

在这里插入图片描述

  下一步工作是修改这段代码,使用 SimpleSelector 代替 listSubjectsWithProperty。

  让我们看看如何对所选语句进行更精细的控制。可以对 SimpleSelector 进行子类化,并修改其 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 "结尾。值得注意的是,基于主语、谓语和对象参数的过滤是在调用 selects(…) 方法之前进行的,因此额外的测试只会应用于匹配的语句。(RDFNode) null 表达式的含义是将 null 转换为 RDFNode 类型的对象。在这种情况下,我们将 null 转换为一个空的 RDFNode 对象,用于表示一个没有特定值的 RDF 资源或字面量。

  在 Jena 中,RDFNode 是一个接口,表示 RDF 图中的节点,它可以是一个资源或字面量。在这个特定的例子中,我们将 (RDFNode) null 用作 SimpleSelector 对象的 object 参数。这意味着我们不关心语句的对象是什么,只需要选择具有 VCARD.FN 属性的语句即可。因此,我们将 object 参数设置为 null,表示选择具有任何对象的 VCARD.FN 属性的语句。

  在代码段中,SimpleSelector是Jena API中的一个类,用于创建三元组选择器,在模型中查找符合特定条件的三元组(即语句)。在这里,创建了一个SimpleSelector的匿名内部类的实例,用于查找具有VCARD.FN谓语且宾语以"Smith"结尾的三元组。在匿名内部类中,重写了selects方法,该方法在每个语句上被调用,以确定是否满足选择条件。

  完整代码可在 教程 8 中找到,输出结果如下:

在这里插入图片描述

  你可能会认为下面的代码:

// do all filtering in the selects method
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)

  虽然这两种方法在功能上可能是等效的,但第一种方法将列举模型中的所有语句,并对每个语句进行测试以确定是否符合条件。这种方法适用于小型模型,但对于大型模型来说,它可能会非常慢。因为它需要处理大量的语句,并且没有利用任何由实现维护的索引来优化性能。第二种方法使用 Selector 对象来选择符合条件的语句。这种方法利用了由实现维护的索引来提高性能。例如,如果我们使用 SimpleSelector 对象来选择具有特定属性和值的语句,Jena 实现将使用索引来查找具有该属性和值的语句,从而提高性能。因此,在大型模型上使用 Model.listStatements(Selector s) 方法通常比使用 Model.listStatements() 方法更快。你可以在一个大型模型上试试看,但在测试之前,最好先泡一杯咖啡,因为处理大型模型可能需要一些时间。

十一、模型上的操作

  Jena提供了三种操作来整体操作模型。这些操作是合并(union)、交集(intersection)和差集(difference)这些常见的集合操作。

  两个模型的合并(union)是代表每个模型的语句集的联合。这是 RDF 设计所支持的关键操作之一。它可以合并来自不同数据源的数据。请看下面两个模型:

在这里插入图片描述

  合并时,两个 http://…JohnSmith 节点会合并为一个,并删除重复的 vcard:FN 弧:

在这里插入图片描述

  让我们看看这样做的代码(完整代码在教程 9 中),看看会发生什么。

// 读取 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="<a href="http://www.w3.org/1999/02/22-rdf-syntax-ns#">http://www.w3.org/1999/02/22-rdf-syntax-ns#</a>"
    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) 方法以类似的方式计算模型的交集和差集;更多详情请参见差集和交集 Javadocs。
Javadoc网址:https://jena.apache.org/documentation/javadoc.html

在这里插入图片描述

十二、 容器(Containers)

  RDF 定义了一种特殊的资源,用于表示事物的集合。这些资源被称为容器。容器的成员可以是文字,也可以是资源。容器有三种:

  • 一个BAG是一个无序集合。
  • 一个ALT是一个无序集合,用于表示可选项。
  • 一个SEQ是一个有序集合。

  容器由一个资源表示。该资源应具有一个rdf:type属性,其值应为rdf:Bag、rdf:Alt或rdf:Seq之一,或这些类型的子类,具体取决于容器的类型。容器的第一个成员是容器的rdf:_1属性的值;容器的第二个成员是容器的rdf:_2属性的值,以此类推。rdf:_nnn属性被称为有序属性。

  例如,一个含有Smith的vcard的简单bag容器的模型可能会看起来是这样的:请添加图片描述

  虽然bag中的成员由 rdf:_1、rdf:_2 等属性表示,但这些属性的排序并不重要。我们可以调换 rdf:_1 和 rdf:_2 属性的值,得到的模型将代表相同的信息。

  Alt 用于表示替代品。例如,假设一个资源代表一种软件产品。它可能有一个属性,用来表示可以从哪里获得。该属性的值可能是一个 Alt 集合,其中包含可以下载该软件的各种网站。Alt 是无序的,只有 rdf:_1 属性具有特殊意义。它代表默认选择。

  虽然容器可以使用资源和属性的基本机制进行处理,但Jena提供了显式的接口和实现类来处理它们。在一个对象操作容器的同时,使用较低级别的方法修改该容器的状态并不是一个好主意。
  在Jena中,为了更方便地处理容器,提供了专门的接口和实现类。这些接口和类提供了一组高级方法和操作,用于管理容器中的成员、顺序和其他属性。通过使用这些接口和类,可以更安全、更一致地操作和修改容器的状态。
  如果在对象中直接使用较低级别的方法来修改容器的状态,可能会导致不一致的结果,或者在并发操作时引发竞态条件。而使用Jena提供的容器接口和实现类,可以确保容器的状态一致性,并提供更高级别的抽象来简化容器操作的编码和维护。
  因此,建议在Jena中使用专门的容器接口和实现类来处理容器,而不是直接使用底层方法修改容器的状态。这样可以提高代码的可读性、可维护性,并减少潜在的错误和并发访问问题。

  让我们修改教程8的代码来创建这个Bag容器

// create a 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'增加到 bag 里
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>

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

// print out the members of the 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");
}

  其输出结果如下:

在这里插入图片描述

  可执行的示例代码可在 教程 10 中找到,它将上述片段整合成一个完整的示例。

  Jena的类提供了用于操作容器的方法,包括添加新成员、将新成员插入容器中间和删除现有成员。Jena容器类目前确保使用的序号属性列表从rdf:_1开始,并且是连续的。然而,RDFCore工作组已经放宽了这个限制,允许对容器进行部分表示。因此,这是Jena可能在未来进行更改的一个领域。

十三、关于字面量和数据类型的更多信息

  RDF 字面量不仅仅是简单的字符串。字面量可能有一个语言标记,用来表示字面量的语言。带有英语语言标记的 "chat "字面与带有法语语言标记的 "chat "字面被认为是不同的。这种相当奇怪的行为是原始 RDF/XML 语法的产物。

  此外,RDF字面量实际上有两种类型。一种类型中,字符串部分只是一个普通的字符串。另一种类型中,字符串部分被期望是一个良好平衡的XML片段。当将RDF模型写入RDF/XML格式时,使用parseType='Literal’属性的特殊构造来表示它。

  在Jena中,当构建文本时,可以设置字面量的这些属性,例如在 教程11 中:

// 创建资源
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>

  要使两个字面量相等,它们必须都是 XML 字面量或都是简单字面量。此外,两者必须都没有语言标记,或者如果有语言标记,则必须相等。对于简单字面量,字符串必须相等。XML 字面量有两种相等的概念。简单的概念是,前面提到的条件为真,字符串也相等。另一种概念是,如果字符串的规范化程度相同,它们就可以相等。为了使两个字面量相等,它们必须满足以下条件:

  • 如果两个字面量都是XML字面量,那么它们的字符串部分必须相等,并且它们的规范化程度(canonicalization)也必须相等。规范化是指将XML片段转换为规范形式,例如去除空格、标准化命名空间等。

  • 如果两个字面量都是简单字面量,那么它们的字符串部分必须相等。

  • 如果两个字面量都没有语言标记,或者如果它们都有语言标记,并且语言标记相等,那么它们被认为是相等的。

  Jena 的接口也支持类型化字面量。老式的方法(如下图所示)将类型化字面量视为字符串的简写:类型化值以通常的 Java 方式转换为字符串,这些字符串存储在模型中。例如,请尝试(注意,对于简单的字面量,我们可以省略 model.createLiteral(…) 调用):

// create the resource
Resource r = model.createResource();

// add the property
r.addProperty(RDFS.label, "11")
 .addProperty(RDFS.label, 11);

// write out the Model
model.write(system.out, "N-TRIPLE");

  输出如下:

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

  由于这两个字面量实际上都只是字符串 “11”,因此只需添加一条语句。

  RDFCore WG 定义了支持 RDF 数据类型的机制。Jena 使用类型化字面机制支持这些机制;本教程不讨论这些机制。

十四、术语表

空白节点: 代表资源,但不表示资源的 URI。空白节点的作用类似于一阶逻辑中的存在限定变量。
Dublin Core: 网络资源元数据的标准。更多信息请访问 Dublin Core web site
字面量: 可以作为属性值的字符串。
对象: 三元组中作为语句值的部分。
谓词: 三元组中的属性部分。
属性: 属性是资源的一个属性。例如,DC.title 是一个属性,RDF.type 也是一个属性。
资源: 某种实体。它可以是网页之类的网络资源,也可以是树木或汽车之类的具体实物。也可以是一个抽象概念,如国际象棋或足球。资源由 URI 命名。
句子(Statement): RDF 模型中的一个弧,通常解释为一个事实。
主语: RDF 模型中弧线来源的资源
三元组: 包含主语、谓语和宾语的结构。语句的另一种说法。
  

十五、脚注

  • RDF 资源的标识符可包括片段标识符,如 http://hostname/rdf/tutorial/#ch-Introduction,因此严格来说,RDF 资源是通过 URI 引用来标识的。
  • 除了字符串之外,字面量还有一个可选的语言编码,用于表示字符串的语言。例如,字面 "two "的语言编码为 “en”,表示英语;字面 "deux "的语言编码为 “fr”,表示法国。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值