require File.join(File.dirname(__FILE__), 'namespace')
require File.join(File.dirname(__FILE__), 'triple')
require File.join(File.dirname(__FILE__), 'store', 'list_store')
require File.join(File.dirname(__FILE__), 'store', 'memory_store')
module RdfContext
# A simple graph to hold triples.
#
# Graphs store triples, and the namespaces associated with those triples, where defined
class Graph
attr_reader :triples
attr_reader :nsbinding
attr_reader :identifier
attr_reader :store
# Create a Graph with the given store and identifier.
#
# The constructor accepts a _store_ option,
# that will be used to store the graph data.
#
# Stores can be context-aware or unaware. Unaware stores take up
# (some) less space but cannot support features that require
# context, such as true merging/demerging of sub-graphs and
# provenance.
#
# The Graph constructor can take an identifier which identifies the Graph
# by name. If none is given, the graph is assigned a BNode for it's identifier.
# For more on named graphs, see: http://en.wikipedia.org/wiki/RDFLib
#
# @param [Hash] options:: Options
# options[:store]:: storage, defaults to a new ListStore instance. May be symbol :list_store or :memory_store
# options[:identifier]:: Identifier for this graph, BNode or URIRef
def initialize(options = {})
@nsbinding = {}
# Instantiate triple store
@store = case options[:store]
when AbstractStore then options[:store]
when :list_store then ListStore.new
when :memory_store then MemoryStore.new
else ListStore.new
end
@identifier = Triple.coerce_subject(options[:identifier]) || BNode.new
end
def inspect
"#{self.class}[id=#{identifier},store=#{store.inspect}]"
end
# Hash of graph, based on graph type and identifier
def hash
[self.class.to_s, self.identifier].hash
end
def context_aware?; @context_aware; end
# Data Store interface
def nsbinding; @store.nsbinding; end
# Destroy the store identified by _configuration_ if supported
# If configuration is nil, remove the graph context
def destroy(configuration = nil)
if configuration
@store.destroy(configuration)
else
@store.remove(Triple.new(nil, nil, nil), self)
end
self.freeze
end
# Commit changes to graph
def commit; @store.commit; end
# Rollback active transactions
def rollback; @store.rollback; end
# Open the graph store
#
# Might be necessary for stores that require opening a connection to a
# database or acquiring some resource.
def open(configuration = {})
@store.open(configuration)
end
# Close the graph store
#
# Might be necessary for stores that require closing a connection to a
# database or releasing some resource.
def close(commit_pending_transaction=false)
@store.open(commit_pending_transaction)
end
##
# Exports the graph to RDF in N-Triples form.
#
# ==== Example
# g = Graph.new; g.add_triple(BNode.new, URIRef.new("http://xmlns.com/foaf/0.1/knows"), BNode.new); g.to_ntriples # => returns a string of the graph in N-Triples form
#
# @return [String]:: The graph in N-Triples.
#
# @author Tom Morris
def to_ntriples
triples.collect do |t|
t.to_ntriples
end * "\n" + "\n"
end
# Output graph using to_ntriples
def to_s; self.to_ntriples; end
##
# Exports the graph to RDF in RDF/XML form.
#
# @return [String]:: The RDF/XML graph
def to_rdfxml
replace_text = {}
rdfxml = ""
xml = builder = Builder::XmlMarkup.new(:target => rdfxml, :indent => 2)
extended_bindings = nsbinding.merge(
"rdf" => RDF_NS,
"rdfs" => RDFS_NS,
"xhv" => XHV_NS,
"xml" => XML_NS
)
rdf_attrs = extended_bindings.values.inject({}) { |hash, ns| hash.merge(ns.xmlns_attr => ns.uri.to_s)}
uri_bindings = self.uri_binding.merge(
RDF_NS.uri.to_s => RDF_NS,
RDFS_NS.uri.to_s => RDFS_NS,
XHV_NS.uri.to_s => XHV_NS,
XML_NS.uri.to_s => XML_NS
)
# Add bindings for predicates not already having bindings
tmp_ns = "ns0"
predicates.each do |p|
unless uri_bindings.has_key?(p.base)
uri_bindings[p.base] = Namespace.new(p.base, tmp_ns)
rdf_attrs["xmlns:#{tmp_ns}"] = p.base
tmp_ns = tmp_ns.succ
end
end
xml.instruct!
xml.rdf(:RDF, rdf_attrs) do
# Add statements for each subject
subjects.each do |s|
xml.rdf(:Description, (s.is_a?(BNode) ? "rdf:nodeID" : "rdf:about") => s) do
triples(Triple.new(s, nil, nil)) do |triple, context|
xml_args = triple.object.xml_args
qname = triple.predicate.to_qname(uri_bindings)
if triple.object.is_a?(Literal) && triple.object.xmlliteral?
replace_text["__replace_with_#{triple.object.object_id}__"] = xml_args[0]
xml_args[0] = "__replace_with_#{triple.object.object_id}__"
end
xml.tag!(qname, *xml_args)
end
end
end
end
# Perform literal substitutions
replace_text.each_pair do |match, value|
rdfxml.sub!(match, value)
end
rdfxml
end
##
# Bind a namespace to the graph.
#
# ==== Example
# g = Graph.new; g.bind(Namespace.new("http://xmlns.com/foaf/0.1/", "foaf")) # => binds the Foaf namespace to g
#
# @param [String] namespace:: the namespace to bind
# @return [Namespace]:: The newly bound or pre-existing namespace.
def bind(namespace)
raise GraphException, "Can't bind #{namespace.inspect} as namespace" unless namespace.is_a?(Namespace)
@store.bind(namespace)
end
# Hash of prefix => Namespace bindings
def nsbinding; @store.nsbinding; end
# Hash of uri => Namespace bindings
def uri_binding; @store.uri_binding; end
# Namespace for prefix
def namespace(prefix); @store.namespace(prefix); end
# Prefix for namespace
def prefix(namespace); @store.prefix(namespace); end
# Number of Triples in the graph
def size; @store.size(self); end
# List of distinct subjects in graph
def subjects; @store.subjects(self); end
# List of distinct predicates in graph
def predicates; @store.predicates(self); end
# List of distinct objects in graph
def objects; @store.objects(self); end
# Indexed statement in serialized graph triples. Equivalent to graph.triples[item]
def [] (item); @store.item(item, self); end
# Adds a triple to a graph directly from the intended subject, predicate, and object.
#
# ==== Example
# g = Graph.new; g.add_triple(BNode.new, URIRef.new("http://xmlns.com/foaf/0.1/knows"), BNode.new) # => results in the triple being added to g; returns an array of g's triples
#
# @param [URIRef, BNode] subject:: the subject of the triple
# @param [URIRef] predicate:: the predicate of the triple
# @param [URIRef, BNode, Literal] object:: the object of the triple
# @return [Graph]:: Returns the graph
# @raise [Error]:: Checks parameter types and raises if they are incorrect.
def add_triple(subject, predicate, object)
self.add(Triple.new(subject, predicate, object))
self
end
##
# Adds an more extant triples to a graph. Delegates to Store.
#
# ==== Example
# g = Graph.new;
# t = Triple.new(BNode.new, URIRef.new("http://xmlns.com/foaf/0.1/knows"), BNode.new);
# g << t
#
# @param [Triple] t:: the triple to be added to the graph
# @return [Graph]:: Returns the graph
def << (triple)
@store.add(triple, self)
self
end
##
# Adds one or more extant triples to a graph. Delegates to Store.
#
# ==== Example
# g = Graph.new;
# t1 = Triple.new(BNode.new, URIRef.new("http://xmlns.com/foaf/0.1/knows"), BNode.new);
# t2 = Triple.new(BNode.new, URIRef.new("http://xmlns.com/foaf/0.1/knows"), BNode.new);
# g.add(t1, t2, ...)
#
# @param [Triple] triples:: one or more triples. Last element may be a hash for options
# options[:context]:: Graph context in which to deposit triples, defaults to default_context or self
# @return [Graph]:: Returns the graph
def add(*triples)
options = triples.last.is_a?(Hash) ? triples.pop : {}
ctx = options[:context] || @default_context || self
triples.each {|t| @store.add(t, ctx)}
self
end
# Remove a triple from the graph. Delegates to store.
# Nil matches all triples and thus empties the graph
def remove(triple); @store.remove(triple, self); end
# Triples from graph, optionally matching subject, predicate, or object.
# Delegates to Store#triples.
#
# @param [Triple, nil] triple:: Triple to match, may be a patern triple or nil
# @return [Array]:: List of matched triples
def triples(triple = Triple.new(nil, nil, nil), &block) # :yields: triple, context
@store.triples(triple, self, &block) || []
end
alias_method :find, :triples
# Detect the presence of a BNode in the graph, either as a subject or an object
#
# @param [BNode] bn:: BNode to find
#
def has_bnode_identifier?(bn)
triples do |triple, context|
return true if triple.subject.eql?(bn) || triple.object.eql?(bn)
end
false
end
# Check to see if this graph contains the specified triple
def contains?(triple)
@store.contains?(triple, self)
end
# Get all BNodes with usage count used within graph
def bnodes
@store.bnodes(self)
end
# Get list of subjects having rdf:type == object
#
# @param [Resource, Regexp, String] object:: Type resource
def get_by_type(object)
triples(Triple.new(nil, RDF_TYPE, object)).map {|t, ctx| t.subject}
end
# Merge a graph into this graph
def merge!(graph)
raise GraphException.new("merge without a graph") unless graph.is_a?(Graph)
# Map BNodes from source Graph to new BNodes
bn = graph.bnodes
bn.keys.each {|k| bn[k] = BNode.new}
graph.triples do |triple, context|
# If triple contains bnodes, remap to new values
if triple.subject.is_a?(BNode) || triple.object.is_a?(BNode)
triple = triple.clone
triple.subject = bn[triple.subject] if triple.subject.is_a?(BNode)
triple.object = bn[triple.object] if triple.object.is_a?(BNode)
end
self << triple
end
end
# Two graphs are equal if each is an instance of the other, considering BNode equivalence.
# This may be done by creating a new graph an substituting each permutation of BNode identifiers
# from self to other until every permutation is exhausted, or a textual equivalence is found
# after sorting each graph.
#
# We just follow Python RDFlib's lead and do a simple comparison
def eql? (other)
#puts "eql? size #{self.size} vs #{other.size}"
return false if !other.is_a?(Graph) || self.size != other.size
return false unless other.identifier.to_s == identifier.to_s
bn_self = bnodes.values.sort
bn_other = other.bnodes.values.sort
#puts "eql? bnodes '#{bn_self.to_sentence}' vs '#{bn_other.to_sentence}'"
return false unless bn_self == bn_other
# Check each triple to see if it's contained in the other graph
triples do |t, ctx|
next if t.subject.is_a?(BNode) || t.object.is_a?(BNode)
#puts "eql? contains '#{t.to_ntriples}'"
return false unless other.contains?(t)
end
true
end
alias_method :==, :eql?
# Parse source into Graph.
#
# Merges results into a common Graph
#
# @param [IO, String] stream:: the RDF IO stream, string, Nokogiri::HTML::Document or Nokogiri::XML::Document
# @param [String] uri:: the URI of the document
# @param [Hash] options:: Options from
# options[:debug]:: Array to place debug messages
# options[:type]:: One of _rdfxml_, _html_, or _n3_
# options[:strict]:: Raise Error if true, continue with lax parsing, otherwise
# @return [Graph]:: Returns the graph containing parsed triples
def parse(stream, uri, options = {}, &block) # :yields: triple
Parser.parse(stream, uri, options.merge(:graph => self), &block)
end
end
end