Uma Introdução a RDF e à API RDF de Jena

Prefácio

Este é um tutorial introdutório ao framework de descrição de recursos (RDF) e Jena, uma API Java para RDF. Ele é escrito para programadores que não estão familiarizados com RDF e que aprendem melhor através de prototipagem, ou, por outros motivos, desejam avançar rapidamente para a implementação. Familiaridade com XML e Java é assumido.

Avançar direto para a implementação, sem conhecer inicialmente o modelo de dados de RDF, levará à frustração e ao desapontamento. No entanto, estudar unicamente o modelo de dados é desgastante e muitas vezes leva a "enigmas metafísicos torturantes". É melhor, então, abordar os conceitos do modelo de dados e como usá-lo, paralelamente. Aprender um pouco o modelo de dados, e praticá-lo. Então aprender um pouco mais e praticar. A teoria leva à prática , e a prática leva à teoria. O modelo de dados é relativamente simples, então esta abordagem não exigirá muito tempo.

RDF possui uma sintaxe XML, e muitos dos que são familiarizados com XML irão pensar em RDF em termos da sintaxe do XML. Isso é um erro. RDF deve ser entendido em termos do seu modelo de dados. Os dados RDF podem ser representados em XML, mas entender a sintaxe é menos importante do que entender o modelo de dados.

Uma implementação da API JENA, incluindo o código fonte dos exemplos usados neste tutorial, podem ser baixados em jena.apache.org/download/index.cgi.

Introdução

O framework de descrição de recursos (RDF) é um padrão (tecnicamente uma recomendação da W3C) para descrever recursos. Mas o que são recursos? Isso é uma questão profunda e a definição precisa ainda é um assunto de debates. Para nossos propósitos, nós podemos pensar em recursos como tudo que podemos identificar. Você é um recurso, assim como sua página pessoal, este tutorial, o número um e a grande baleia branca em Moby Dick.

Nossos exemplos neste tutorial serão sobre pessoas. Elas usam uma representação RDF de cartão de negócios (VCARDS). RDF é melhor representado como um diagrama de nós e arcos. Um simples vcard se assemelha a isto em RDF:

figure 1

O recurso, John Smith, é exibido como uma elipse e identificado por um Identificador Uniforme de Recurso (URI)1, neste caso "http://.../JohnSmith". Se você tentar acessar o recurso usando seu navegador, não vai obter sucesso. Se você não tem familiaridade com URI's, pense neles como nomes estranhos.

Recursos possuem propriedades. Nesses exemplos, nós estamos interessados nos tipos de propriedades que apareceriam no cartão de negócios de Jonh Smith. A figura 1 mostra somente uma propriedade, o nome completo (full name) de Jonh Smith. Uma propriedade é representada por um arco, intitulado com o nome da propriedade. O nome da propriedade é também um URI, mas como os URIs são longos e incomodas, o diagrama o exibe em forma XML qname. A parte antes de ':' é chamada de prefixo namespace e representa um namespace. A parte depois de ':' é um nome local e representa o nome naquele namespace. Propriedades são normalmente representadas nesta forma de qname quando escrito como RDF XML, e isso é uma maneira prática de representá-los em diagramas e textos. No entanto, propriedades são rigorosamente representadas por um URI. A forma nsprefix:localname é um atalho para o URI do namespace concatenado com o nome local. Não há exigências de que o URI de uma propriedade resulte em algo quando acessado do navegador.

Toda propriedade possui um valor. Neste caso, o valor é uma literal, que por hora podemos pensar nelas como uma cadeia de caracteres2. Literais são exibidas em retângulos.

Jena é uma API Java que pode ser usada para pra criar e manipular grafos RDF como o apresentado no exemplo. Jena possui classes para representar grafos, recursos, propriedades e literais. As interfaces que representam recursos, propriedades e literais são chamadas de modelo e é representada pela interface Model.

O código para criar este grafo, ou modelo, é simples:

// some definitions
static String personURI    = "http://somewhere/JohnSmith";
static String fullName     = "John Smith";

// create an empty Model
Model model = ModelFactory.createDefaultModel();

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

// add the property
johnSmith.addProperty(VCARD.FN, fullName);

Ele começa com algumas definições de constantes e então cria um Model vazio, usando o método createDefaultModel() de ModelFactory para criar um modelo na memória. Jena possui outras implementações da interface Model, e.g. uma que usa banco de dados relacionais: esses tipos de modelo são também disponibilizados a partir de ModelFactory.

O recurso Jonh Smith é então criado, e uma propriedade é adicionada a ele. A propriedade é fornecida pela a classe "constante" VCARD, que mantém os objetos que representam todas as definições no esquema de VCARD. Jena provê classes constantes para outros esquemas bem conhecidos, bem como os próprios RDF e RDFs , Dublin Core e OWL.

O código para criar o recurso e adicionar a propriedade pode ser escrito de forma mais compacta usando um estilo cascata:

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

Os códigos desse exemplo podem ser encontrados no diretório /src-examples no pacote de distribuição do Jena como tutorial 1. Como exercício, pegue este código e modifique-o para criar um próprio VCARD para você.

Agora vamos adicionar mais detalhes ao vcard, explorando mais recursos de RDF e Jena.

No primeiro exemplo, o valor da propriedade foi um número. As propriedades RDF podem também assumir outros recursos como valor. Usando uma técnica comum em RDF, este exemplo mostra como representar diferentes partes do nome de Jonh Smith:

figure 2

Aqui, nós adicionamos uma nova propriedade, vcard:N, para representar a estrutura do nome de Jonh Smith. Há muitas coisas interessantes sobre este modelo. Note que a propriedade vcard:N usa um recurso como seu valor. Note também que a elipse que representa a composição do nome não possui URI. Isso é conhecido como blank Node.

O código Jena para construir este exemplo é, novamente, muito simples. Primeiro algumas declarações e a criação do modelo vazio.

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

// create an empty Model
Model model = ModelFactory.createDefaultModel();

// create the resource
//   and add the properties cascading style
Resource johnSmith
  = model.createResource(personURI)
         .addProperty(VCARD.FN, fullName)
         .addProperty(VCARD.N,
                      model.createResource()
                           .addProperty(VCARD.Given, givenName)
                           .addProperty(VCARD.Family, familyName));

Os códigos desse exemplo podem ser encontrados no diretório /src-examples no pacote de distribuição do Jena como tutorial 2.

Sentenças

Cada arco no modelo RDF é chamado de sentença. Cada sentença define um fato sobre o recurso. Uma sentença possui três partes:

  • o sujeito é o recurso de onde o arco começa.
  • o predicado é a propriedade que nomeia o arco.
  • o objeto é o recurso ou literal apontado pelo arco.

Uma sentença é algumas vezes chamadas de tripla, por causa de suas três partes.

Um modelo RDF é representado como um conjunto de sentenças. Cada chamada a addProperty no tutorial2 adiciona uma nova sentença. (Já que um modelo é um conjunto de sentenças, adicionar sentenças duplicadas não afeta em nada). A interface modelo de Jena define o método listStatements() que retorna um StmtIterator, um subtipo de Iterator Java sobre todas as sentenças de um modelo. StmtIterator possui o método nextStatement() que retorna a próxima sentença do iterador (o mesmo que next() faz, já convertido para Statement). A interface Statement provê métodos de acesso ao sujeito, predicado e objeto de uma sentença.

Agora vamos usar essa interface para estender tutorial2 para listar todas as sentenças criadas e imprimi-las. O código completo deste exemplo pode ser encontrado em tutorial 3.

// list the statements in the Model
StmtIterator iter = model.listStatements();

// print out the predicate, subject and object of each statement
while (iter.hasNext()) {
    Statement stmt      = iter.nextStatement();  // get next statement
    Resource  subject   = stmt.getSubject();     // get the subject
    Property  predicate = stmt.getPredicate();   // get the predicate
    RDFNode   object    = stmt.getObject();      // get the object

    System.out.print(subject.toString());
    System.out.print(" " + predicate.toString() + " ");
    if (object instanceof Resource) {
       System.out.print(object.toString());
    } else {
        // object is a literal
        System.out.print(" \"" + object.toString() + "\"");
    }

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

Já que o objeto de uma sentença pode ser tanto um recurso quanto uma literal, o método getObject() retorna um objeto do tipo RDFNode, que é uma superclasse comum de ambos Resource e Literal. O objeto em si é do tipo apropriado, então o código usa instanceof para determinar qual e processá-lo de acordo.

Quando executado, o programa deve produzir a saída:

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

Agora você sabe o porquê de ser simples elaborar modelos. Se você olhar atentamente, você perceberá que cada linha consiste de três campos representando o sujeito, predicado e objeto de cada sentença. Há quatro arcos no nosso modelo, então há quatro sentenças. O "anon:14df86:ecc3dee17b:-7fff" é um identificador interno gerado pelo Jena. Não é uma URI e não deve ser confundido como tal. É simplesmente um nome interno usado pela implementação do Jena.

O W3C RDFCore Working Group definiu uma notação similar chamada N-Triples. O nome significa "notação de triplas". Nós veremos na próxima sessão que o Jena possui uma interface de escrita de N-Triples também.

Escrita de RDF

Jena possui métodos para ler e escrever RDF como XML. Eles podem ser usados para armazenar o modelo RDF em um arquivo e carregá-lo novamente em outro momento.

O Tutorial 3 criou um modelo e o escreveu no formato de triplas. Tutorial 4 modifica o tutorial 3 para escrever o modelo na forma de RDF XML numa stream de saída. O código, novamente, é muito simples: model.write pode receber um OutputStream como argumento.

// now write the model in XML form to a file
model.write(System.out);

A saída deve parecer com isso:

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

As especificações de RDF especificam como representar RDF como XML. A sintaxe de RDF XML é bastante complexa. Recomendamos ao leitor dar uma olhada no primer sendo desenvolvido pelo RDFCore WG para uma introdução mais detalhada. Entretanto, vamos dar uma olhada rápida em como interpretar a saída acima.

RDF é normalmente encapsulada num elemento <rdf:RDF>. O elemento é opcional se houver outras maneiras de saber se aquele XML é RDF, mas normalmente ele é presente. O elemento RDF define os dois namespaces usados no documento. Há um elemento <rdf:Description> que descreve o recurso cuja URI é "http://somewhere/JohnSmith". Se o atributo rdf:about estivesse ausente, esse elemento representaria um blank node.

O elemento <vcard:FN> descreve uma propriedade do recurso. O nome da propriedade é o "FN" no namespace do vcard. RDF o converte para uma referência URI concatenando a referência URI do prefixo presente no namespace de vcard e "FN", o nome local parte do nome. Isto nos dá a referência URI " http://www.w3.org/2001/vcard-rdf/3.0#FN". O valor da propriedade é a literal "Jonh Smith".

O elemento <vcard:N> é um recurso. Neste caso, o recurso é representado por uma referência URI relativa. RDF o converte para uma referência URI absoluta concatenando com o URI base do documento.

Há um erro nesse RDF XML: ele não representa exatamente o modelo que criamos. Foi dado uma URI ao blank node do modelo. Ele não é mais um blank node portanto. A sintaxe RDF/XML não é capaz de representar todos os modelos RDF; por exemplo, ela não pode representar um blank node que é o objeto de duas sentenças. O escritor que usamos para escrever este RDF/XML não é capaz de escrever corretamente o subconjunto de modelos que podem ser escritos corretamente. Ele dá uma URI a cada blank node, tornando-o não mais blank.

Jena possui uma interface extensível que permite novos escritores para diferentes linguagens de serialização RDF. Jena possuem também um escritor RDF/XML mais sofisticado que pode ser invocado ao especificar outro argumento à chamada de método RDFDataMgr.write:

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

Este escritor, chamado também de PrettyWriter, ganha vantagem ao usar as particularidades da sintaxe abreviada de RDF/XML ao criar um modelo mais compacto. Ele também é capaz de preservar os blank nodes onde é possível. Entretanto, não é recomendável para escrever modelos muito grandes, já que sua desempenho deixa a desejar. Para escrever grandes arquivos e preservar os blank nodes, escreva no formato de N-Triplas:

// now write the model in XML form to a file
RDFDataMgr.write(System.out, model, Lang.NTRIPLES);

Isso produzirá uma saída similar à do tutorial 3, que está em conformidade com a especificação de N-Triplas.

Leitura de RDF

Tutorial 5 demonstra a leitura num modelo de sentenças gravadas num RDF XML. Com este tutorial, nós teremos criado uma pequena base de dados de vcards na forma RDF/XML. O código a seguir fará leitura e escrita. Note que para esta aplicação rodar, o arquivo de entrada precisa estar no diretório da aplicação.

// create an empty model
Model model = ModelFactory.createDefaultModel();

// use the RDFDataMgr to find the input file
InputStream in = RDFDataMgr.open( inputFileName );
if (in == null) {
    throw new IllegalArgumentException(
                                 "File: " + inputFileName + " not found");
}

// read the RDF/XML file
model.read(in, null);

// write it to standard out
model.write(System.out);

O segundo argumento da chamada de método read() é a URI que será usada para resolver URIs relativas. Como não há referências URI relativas no arquivo de teste, ele pode ser vazio. Quando executado, tutorial 5 produzirá uma saída XML como esta:

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

Controlando prefixos

Definições explícitas de prefixos

Na sessão anterior, nós vimos que a saída XML declarou um prefixo namespace
vcard e o usou para abreviar URIs. Enquanto que RDF usa somente URIs completas, e não sua forma encurtada, Jena provê formas de controlar namespaces usados na saída com seu mapeamento de prefixos. Aqui vai um exemplo simples.

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" );
m.setNsPrefix( "nsA", nsA );
m.write( System.out );
System.out.println( "# -- nsA and cat defined" );
m.setNsPrefix( "cat", nsB );
m.write( System.out );

A saída deste fragmento são três blocos de RDF/XML, com três diferentes mapeamentos de prefixos. Primeiro o padrão, sem prefixos diferentes dos padrões:

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

Nós vimos que o namespace rdf é declarado automaticamente, já que são requeridos para tags como <RDF:rdf> e <rdf:resource>. Declarações de namespace são também necessárias para o uso das duas propriedades P e Q, mas já que seus namespaces não foram introduzidos no modelo, eles recebem nomes namespaces inventados j.0 e j.1.

O método setNsPrefix(String prefix, String URI) declara que o namespace da URI deve ser abreviado por prefixos. Jena requer que o prefixo seja um namespace XML correto, e que o URI termine com um caractere sem-nome. O escritor RDF/XML transformará essas declarações de prefixos em declarações de namespaces XML e as usará nas suas saídas:

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

O outro namespace ainda recebe o nome criado automaticamente, mas o nome nsA é agora usado nas tags de propriedades. Não há necessidade de que o nome do prefixo tenha alguma relação com as variáveis do código Jena:

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

Ambos os prefixos são usados na saída, e não houve a necessidade de prefixos gerados automaticamente.

Definições implícitas de prefixos

Assim como as declarações de prefixos definidas por chamadas a setNsPrefix, Jena vai lembrar-se dos prefixos que foram usados na entrada para model.read().

Pegue a saída produzida pelo fragmento anterior e cole-o dentro de algum arquivo, com a URL file:/tmp/fragment.rdf say. E execute o código:

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

Você verá que os prefixos da entrada são preservados na saída. Todos os prefixos são escritos, mesmo se eles não forem usados em lugar algum. Você pode remover um prefixo com removeNsPrefix(String prefix) se você não o quiser na saída.

Como N-Triplas não possuem nenhuma forma reduzida de escrever URIs, não há prefixos nem na entrada nem na saída. A notação N3, também suportada pelo Jena, possui nomes prefixados reduzidos, e grava-os na entrada e usa-os na saída.

Jena possui outras operações sobre mapeamento de prefixos de um modelo, como umMap de Java extraído a partir dos mapeamentos existentes, ou a adição de um grupo inteiro de mapeamentos de uma só vez; olhe a documentação de PrefixMapping para mais detalhes.

Pacotes Jena RDF

Jena é uma API JAVA para aplicações de web semântica. O pacote RDF chave para o desenvolvedor é org.apache.jena.rdf.model. A API tem sido definida em termos de interfaces, logo o código da aplicação pode trabalhar com diferentes implementações sem causar mudanças. Esse pacote contém interfaces para representar modelos, recursos, propriedades, literais, sentenças e todos os outros conceitos chaves de RDF, e um ModelFactory para criação de modelos. Portanto, o código da aplicação permanece independente da implementação, o melhor é usar interfaces onde for possível e não implementações específicas de classes.

O pacote org.apache.jena.tutorial contém o código fonte funcional de todos os exemplos usados neste tutorial.

Os pacotes org.apache.jena...impl contêm a implementação de classes que podem ser comuns a várias implementações. Por exemplo, eles definem as classes ResourceImpl, PropertyImpl, e LiteralImpl que podem ser usadas diretamente ou então herdadas por diferentes implementações. As aplicações devem raramente usar essas classes diretamente. Por exemplo, em vez de criar um nova instância de ResourceImpl, é melhor usar o método createResource do modelo que estiver sendo usado. Desta forma, se a implementação do modelo usar uma implementação otimizada de Resource, então não serão necessárias conversões entre os dois tipos.

Navegação em Modelos

Até agora, este tutorial mostrou como criar, ler e escrever modelos RDF. Chegou o momento de mostrar como acessar as informações mantidas num modelo.

Dada a URI de um recurso, o objeto do recurso pode ser recuperado de um modelo usando o método Model.getResource(String uri). Este método é definido para retornar um objeto Resource se ele existir no modelo, ou, caso contrário, criar um novo. Por exemplo, para recuperar o recurso Adam Smith do modelo lido a partir do arquivo no tutorial 5:

// retrieve the John Smith vcard resource from the model
Resource vcard = model.getResource(johnSmithURI);

A interface Resource define numerosos métodos para acessar as propriedades de um recurso. O método Resource.getProperty(Property p) acessa uma propriedade do recurso. Este método não segue a convenção usual de Java de acesso já que o tipo do objeto retorna é Statement, e não Property como era de se esperar. Retornando toda a sentença permite à aplicação acessar o valor da propriedade usando um de seus métodos de acesso que retornam o objeto da sentença. Por exemplo, para recuperar o recurso que é o valor da propriedade vcard:N

// retrieve the value of the N property
Resource name = (Resource) vcard.getProperty(VCARD.N)
                                .getObject();

De modo geral, o objeto de uma sentença pode ser um recurso ou uma literal, então a aplicação, sabendo que o valor precisar ser um recurso, faz o cast do objeto retornado. Uma das coisas que Jena tenta fazer é fornecer tipos específicos de métodos, então a aplicação não tem que fazer cast, e checagem de tipos pode ser feita em tempo de compilação. O fragmento de código acima poderia ser mais convenientemente escrito assim:

// retrieve the value of the N property
Resource name = vcard.getProperty(VCARD.N)
                     .getResource();

Similarmente, o valor literal de uma propriedade pode ser recuperado:

String fullName = vcard.getProperty(VCARD.FN)
                        .getString();

Neste exemplo, o recurso vcard possui somente as propriedades vcard:FN e vcard:N. RDF permite a um recurso repetir uma propriedade; por exemplo, Adam pode ter mais de um apelido. Vamos dar dois apelidos a ele:

// add two nickname properties to vcard
vcard.addProperty(VCARD.NICKNAME, "Smithy")
     .addProperty(VCARD.NICKNAME, "Adman");

Como notado anteriormente, Jena representa um modelo RDF como um conjunto de sentenças, então, adicionar uma sentença com um sujeito, predicado e objeto igual a um já existente não terá efeito. Jena não define qual do dois apelidos será retornado. O resultado da chamada a vcard.getProperty(VCARD.NICKNAME) é indeterminado. Jena vai retornar um dos valores, mas não há garantia nem mesmo de que duas chamadas consecutivas irá retornar o mesmo valor.

Se for possível que uma propriedade ocorra mais de uma vez, então o método Resource.listProperties(Property p) pode ser usado para retornar um iterador para lista-las. Este método retorna um iterador que retorna objetos do tipo Statement. Nós podemos listar os apelidos assim:

// 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());
}

Esse código pode ser encontrado em tutorial 6. O iterador iter reproduz todas as sentenças com sujeito vcard e predicado VCARD.NICKNAME, então, iterar sobre ele permite recuperar cada sentença usando nextStatement(), pegar o campo do objeto, e convertê-lo para string. O código produz a seguinte saída quando executado:

The nicknames of "John Smith" are:
    Smithy
    Adman

Todas as propriedades de um recurso podem ser listadas usando o método listProperties() sem argumentos.

Consultas em Modelos

A sessão anterior mostrou como navegar um modelo a partir de um recurso com uma URI conhecida. Essa sessão mostrará como fazer buscas em um modelo. O núcleo da API Jena suporta um limitada primitiva de consulta. As consultas mais poderosas de SPARQL são descritas em outros lugares.

O método Model.listStatements(), que lista todos as sentenças de um modelo, é talvez a forma mais crua de se consultar um modelo. Este uso não é recomendado em modelos muito grandes. Model.listSubjects() é similar, mas retorna um iterador sobre todos os recursos que possuem propriedades, ie são sujeitos de alguma sentença.

Model.listSubjectsWithProperty(Property p, RDFNode o) retornará um iterador sobre todos os recursos com propriedade p de valor o. Se nós assumirmos que somente recursos vcard terão a propriedade vcard:FN e que, em nossos dados, todos esses recursos têm essa propriedade, então podemos encontrar todos os vCards assim:

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

Todos esses métodos de consulta são açúcar sintático sobre o método primitivo de consulta model.listStatements(Selector s). Esse método retorna um iterador sobre todas as sentenças no modelo 'selecionado' por s. A interface de selector foi feita para ser extensível, mas por hora, só há uma implementação dela, a classe SimpleSelector do pacote org.apache.jena.rdf.model. Usar SimpleSelector é uma das raras ocasiões em Jena onde é necessário usar uma classe especifica em vez de uma interface. O construtor de SimpleSelector recebe três argumentos:

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

Esse selector vai selecionar todas as sentenças em que o sujeito casa com subject, um predicado que casa com predicate e um objeto que casa com object. Se null é passado para algum dos argumentos, ele vai casar com qualquer coisa, caso contrário, ele vai casar com os recursos ou literais correspondentes. (Dois recursos são iguais se eles possuem o mesmo URI ou são o mesmo blank node; dois literais são iguais se todos os seus componentes forem iguais.) Assim:

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

vai selecionar todas as sentenças do modelo.

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

vai selecionar todas as sentenças com o predicado VCARD.FN, independente do sujeito ou objeto. Como um atalho especial,

listStatements( S, P, O )

é equivalente a

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

O código a seguir, que pode ser encontrado em tutorial 7 que lista os nomes completos de todos os vcards do banco de dados.

// 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");
}

Isso deve produzir uma saída similar a:

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

Seu próximo exercício é modificar o código para usar SimpleSelector em vez de listSubjectsWithProperty.

Vamos ver como implementar um controle mais refinado sobre as sentenças selecionadas. SimpleSelector pode ser herdado ter seus selects modificado para mais filtragens:

// select all the resources with a VCARD.FN property
// whose value ends with "Smith"
StmtIterator iter = model.listStatements(
    new SimpleSelector(null, VCARD.FN, (RDFNode) null) {
        public boolean selects(Statement s)
            {return s.getString().endsWith("Smith");}
    });

Esse código usa uma técnica elegante de Java para sobrescrever a definição de um método quando criamos uma instância da classe. Aqui, o método selects(...) garante que o nome completo termine com “Smith”. É importante notar que a filtragem baseada nos argumentos sujeito, predicado e objeto tem lugar antes que o método selects(...) seja chamado, então esse teste extra só será aplicado para casar sentenças.

O código completo pode ser encontrado no tutorial 8 e produz uma saída igual a:

The database contains vcards for:
  John Smith
  Becky Smith

Você pode imaginar que:

// 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))
                  &amp;&amp; (predicate == null || s.getPredicate().equals(predicate))
                  &amp;&amp; (object == null    || s.getObject().equals(object)) ;
          }
     }
     });

é equivalente a:

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

Embora possam ser funcionalmente equivalentes, a primeira forma vai listar todas as sentenças do modelo e testar cada uma individualmente, a segunda forma permite índices mantidos pela implementação para melhor a perfomance. Tente isso em modelos grandes e veja você mesmo, mas prepare uma chícara de café antes.

Operações em Modelos

Jena provê três operações para manipular modelos. Elas são operações comuns de conjunto: união, intersecção e diferença.

A união de dois modelos é a união do conjunto de sentenças que representa cada modelo. Esta é uma das operações chaves que RDF suporta. Isso permite que fontes de dados discrepantes sejam juntadas. Considere o seguintes modelos:

figure 4and figure 5

Quando eles são juntados, os dois nós http://...JohnSmith são unidos em um, e o arco vcard:FN duplicado é descartado para produzir:

figure 6

Vamos ver o código (o código completo está em tutorial 9) e ver o que acontece.

// read the RDF/XML files
model1.read(new InputStreamReader(in1), "");
model2.read(new InputStreamReader(in2), "");

// merge the Models
Model model = model1.union(model2);

// print the Model as RDF/XML
model.write(system.out, "RDF/XML-ABBREV");

A saída produzida pelo PrettyWriter se assemelha a:

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

Mesmo que você não seja familiarizado com os detalhes da sintaxe RDF/XML, deve ser relativamente claro que os modelos foram unidos como esperado A interseção e a diferença de modelos podem ser computados de maneira semelhante, usando os métodos .intersection(Model) e .difference(Model); veja a documentação de difference e intersection para mais detalhes.

Containers

RDF defina um tipo especial de recursos para representar coleções de coisas. Esses recursos são chamados de containers. Os membros de um container podem ser tanto literais quanto recursos. Há três tipos de containers:

  • um BAG é uma coleção desordenada
  • uma ALT é uma coleção desordenada para representar alternativas
  • uma SEQ é uma coleção ordenada

Um container é representado por um recurso. Esse recurso terá uma propriedade rdf:type cujo valor deve ser rdf:Bag, rdf:Alt, rdf:Seq, ou uma subclasse deles, dependendo do tipo do container. O primeiro membro do container é o valor da propriedade rdf:_1 do container; o segundo membro é o valor da propriedade rdf:_2 e assim por diante. As propriedades rdf:_nnn são conhecidas como propriedades ordinais.

Por exemplo, o modelo para uma bag simples contendo os vcards dos Smith pode se assemelhar a:

figure 3

Embora os membros do bag serem representados pelas propriedades rdf:_1, rdf:_2 etc. a ordem das propriedades é insignificante. Nós poderíamos trocar os valores das propriedades rdf_1 e rdf_2 o resultado do modelo representaria a mesma informação.

ALTs representam alternativas. Por exemplo, vamos supor um recurso representando um software. Ele poderia ter uma propriedade indicando onde ele foi obtido. O valor dessa propriedade pode ser uma coleção ALT contendo vários sites de onde ele poderia ser baixado. ALTs são desordenados a menos que a propriedade rdf:_1 tenha significado especial. Ela representa a escolha padrão.

Embora os containers sejam manipulados como recursos e propriedades, Jena têm interfaces e implementações de classes explicitas para manipulá-los. Não é uma boa ideia ter um objeto manipulando um container e ao mesmo tempo modificando o estado do container usando métodos de mais baixo nível.

Vamos modificar o tutorial 8 para criar essa bag:

// create a bag
Bag smiths = model.createBag();

// select all the resources with a VCARD.FN property
// whose value ends with "Smith"
StmtIterator iter = model.listStatements(
    new SimpleSelector(null, VCARD.FN, (RDFNode) null) {
        public boolean selects(Statement s) {
                return s.getString().endsWith("Smith");
        }
    });
// add the Smith's to the bag
while (iter.hasNext()) {
    smiths.add(iter.nextStatement().getSubject());
}

Se nós imprimirmos esse modelo, vamos obter algo do tipo:

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

que representa o recurso Bag.

A interface do container fornece um iterador para listar o conteúdo do container:

// 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");
}

que produz a seguinte saída:

The bag contains:
  John Smith
  Becky Smith

O código executável pode ser encontrado em tutorial 10, que coloca esses fragmentos de código juntos num exemplo completo.

As classes de Jena oferecem métodos para manipular containers, incluindo adição de novos membros, inserção de novos membros no meio de um container e a remoção de membros existentes. As classes de container Jena atualmente garantem que a lista ordenada de propriedades usadas começam com rdf:_1 e é contíguo. O RDFCore WG relaxou essa regra, permitindo uma representação parcial dos containers. Isso, portanto, é uma área de Jena que pode ser mudada no futuro.

Mais sobre Literais e Datatypes

Literais RDF não são apenas strings. Literais devem ter uma tag para indicar a linguagem da literal. O diálogo de uma literal em Inglês é considerado diferente de um diálogo de uma literal em Francês. Esse comportamento estranho é um artefato da sintaxe RDF/XML original.

Há na realidade dois tipos de Literais. Em uma delas, o componente string é somente isso, uma string ordinária. Na outra, o componente string é esperado que fosse um bem balanceado fragmento de XML. Quando um modelo RDF é escrito como RDF/XML, uma construção especial usando um atributo parseType='Literal' é usado para representar isso.

Em Jena, esses atributos de uma literal podem ser setados quando a literal é construída, e.g. no tutorial 11:

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

// add the property
r.addProperty(RDFS.label, model.createLiteral("chat", "en"))
 .addProperty(RDFS.label, model.createLiteral("chat", "fr"))
 .addProperty(RDFS.label, model.createLiteral("&lt;em&gt;chat&lt;/em&gt;", true));

// write out the Model
model.write(system.out);

produz

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

Para que duas literais sejam consideradas iguais, elas devem ser ambas literais XML ou ambas literais simples. Em adição a isso, ambas não devem possuir tag de linguagem, ou se as tags de linguagem estiverem presentes, elas devem ser iguais. Para simples literais strings, elas devem ser iguais. Literais XML têm duas noções de igualdade. A noção simples é que as condições anteriores são verdade e as strings também são iguais. A outra noção é de que elas podem ser iguais se a canonização das strings forem iguais.

As interfaces de Jena também suportam literais tipadas. A maneira antiga (mostrada abaixo) trata literais tipadas como atalhos para strings: valores tipados são convertidos por Java em strings, e essas strings são armazenadas no modelo. Por exemplo, tente (observando que para literais simples, nós podemos omitir a chamada code>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 saída produzida é:

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

Já que ambas as literais são apenas a string “’11”, então somente uma sentença é adicionada

O RDFCore WG definiu mecanismos para suportar datatypes em RDF. Jena os suporta usando mecanismos de literais tipadas; isso não é discutido neste tutorial.

Glossário

Blank Node
representam recursos, mas não indica a URI para este recurso. Blank nodes atuam como variáveis qualificadas existencialmente em lógica de primeira ordem.
Dublin Core
é um padrão para metadados sobre recursos web. Mais informações podem ser encontradas no site Dublin Core web site.
Literal
é uma cadeia de caracteres que pode ser o valor de uma propriedade.
Objeto
é a parte da tripla que é o valor de uma sentença.
Predicado
é a propriedade da tripla.
Propriedade
é um atributo do recurso. Por exemplo, DC.Title é uma propriedade, como também RDF.type.
Recurso
é alguma entidade. Ele pode ser um recurso web como uma página, ou pode ser uma coisa concreta como um carro ou árvore. Pode ser uma ideia abstrata como xadrez ou futebol. Recursos são nomeados por URIs.
Sentença
é um arco no modelo RDF, normalmente interpretado como um fato.
Sujeito
é o recurso que é a fonte do arco num modelo RDF.
Tripla
é a estrutura contendo o sujeito, o predicado e o objeto. Outro termo para sentença.