lib/rdf/ldp/rdf_source.rb in rdf-ldp-0.3.0 vs lib/rdf/ldp/rdf_source.rb in rdf-ldp-0.4.0
- old
+ new
@@ -7,29 +7,30 @@
# `NonRDFSources`. RDFSources are implemented as a resource with:
#
# - a `#graph` representing the "entire persistent state"
# - a `#metagraph` containing internal properties of the RDFSource
#
- # Persistence schemes must be able to reconstruct both `#graph` and
- # `#metagraph` accurately and separately (e.g. by saving them as distinct
- # named graphs). Statements in `#metagraph` are considered canonical for the
- # purposes of server-side operations; in the `RDF::LDP` core, this means they
- # determine interaction model.
+ # Repository implementations must be able to reconstruct both `#graph` and
+ # `#metagraph` accurately and separately (e.g., by saving them as distinct
+ # named graphs).
+ #
+ # The implementations of `#create` and `#update` in `RDF::LDP::Resource` are
+ # overloaded to handle the edits to `#graph` within the same transaction as
+ # the base `#metagraph` updates. `#to_response` is overloaded to return an
+ # unnamed `RDF::Graph`, to be transformed into an HTTP Body by
+ # `Rack::LDP::ContentNegotiation`.
#
- # Note that the contents of `#metagraph`'s are *not* the same as
- # LDP-server-managed triples. `#metagraph` contains statements internal
- # properties of the RDFSource which are necessary for the server's management
- # purposes, but MAY be absent from the representation of its state in `#graph`.
+ # @note the contents of `#metagraph`'s are *not* the same as
+ # LDP-server-managed triples. `#metagraph` contains internal properties of the
+ # RDFSource which are necessary for the server's management purposes, but MAY
+ # be absent from (or in conflict with) the representation of its state in
+ # `#graph`.
#
# @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-rdf-source definition
# of ldp:RDFSource in the LDP specification
class RDFSource < Resource
- # @!attribute [rw] graph
- # a graph representing the current persistent state of the resource.
- attr_accessor :graph
-
class << self
##
# @return [RDF::URI] uri with lexical representation
# 'http://www.w3.org/ns/ldp#RDFSource'
#
@@ -40,41 +41,70 @@
end
##
# @see RDF::LDP::Resource#initialize
def initialize(subject_uri, data = RDF::Repository.new)
- @graph = RDF::Graph.new(subject_uri, data: data)
+ @subject_uri = subject_uri
+ @data = data
super
self
end
##
+ # @return [RDF::Graph] a graph representing the current persistent state of
+ # the resource.
+ def graph
+ @graph ||= RDF::Graph.new(@subject_uri, data: @data)
+ end
+
+ ##
# Creates the RDFSource, populating its graph from the input given
#
+ # @example
+ # repository = RDF::Repository.new
+ # ldprs = RDF::LDP::RDFSource.new('http://example.org/moomin', repository)
+ # ldprs.create('<http://ex.org/1> <http://ex.org/prop> "moomin" .', 'text/turtle')
+ #
# @param [IO, File, #to_s] input input (usually from a Rack env's
# `rack.input` key) used to determine the Resource's initial state.
# @param [#to_s] content_type a MIME content_type used to read the graph.
#
- # @yield gives the new contents of `graph` to the caller's block before
- # altering the state of the resource. This is useful when validation is
- # required or triples are to be added by a subclass.
- # @yieldparam [RDF::Enumerable] the contents parsed from input.
+ # @yield gives an in-progress transaction (changeset) to collect changes to
+ # graph, metagraph and other resources' (e.g. containers) graphs.
+ # @yieldparam tx [RDF::Transaction] a transaction targeting `#graph` as the
+ # default graph name
#
+ # @example altering changes before execution with block syntax
+ # content = '<http://ex.org/1> <http://ex.org/prop> "moomin" .'
+ #
+ # ldprs.create(content, 'text/turtle') do |tx|
+ # tx.insert([RDF::URI('s'), RDF::URI('p'), 'custom'])
+ # tx.insert([RDF::URI('s'), RDF::URI('p'), 'custom', RDF::URI('g')])
+ # end
+ #
+ # @example validating changes before execution with block syntax
+ # content = '<http://ex.org/1> <http://ex.org/prop> "moomin" .'
+ #
+ # ldprs.create(content, 'text/turtle') do |tx|
+ # raise "cannot delete triples on create!" unless tx.deletes.empty?
+ # end
+ #
# @raise [RDF::LDP::RequestError]
# @raise [RDF::LDP::UnsupportedMediaType] if no reader can be found for the
# graph
# @raise [RDF::LDP::BadRequest] if the identified reader can't parse the
# graph
# @raise [RDF::LDP::Conflict] if the RDFSource already exists
#
# @return [RDF::LDP::Resource] self
def create(input, content_type, &block)
- super
- statements = parse_graph(input, content_type)
- yield statements if block_given?
- graph << statements
- self
+ super do |transaction|
+ transaction.graph_name = subject_uri
+ statements = parse_graph(input, content_type)
+ transaction << statements
+ yield transaction if block_given?
+ end
end
##
# Updates the resource. Replaces the contents of `graph` with the parsed
# input.
@@ -82,70 +112,50 @@
# @param [IO, File, #to_s] input input (usually from a Rack env's
# `rack.input` key) used to determine the Resource's new state.
# @param [#to_s] content_type a MIME content_type used to interpret the
# input.
#
- # @yield gives the new contents of `graph` to the caller's block before
- # altering the state of the resource. This is useful when validation is
- # required or triples are to be added by a subclass.
- # @yieldparam [RDF::Enumerable] the triples parsed from input.
+ # @yield gives an in-progress transaction (changeset) to collect changes to
+ # graph, metagraph and other resources' (e.g. containers) graphs.
+ # @yieldparam tx [RDF::Transaction] a transaction targeting `#graph` as the
+ # default graph name
#
+ # @example altering changes before execution with block syntax
+ # content = '<http://ex.org/1> <http://ex.org/prop> "moomin" .'
+ #
+ # ldprs.update(content, 'text/turtle') do |tx|
+ # tx.insert([RDF::URI('s'), RDF::URI('p'), 'custom'])
+ # tx.insert([RDF::URI('s'), RDF::URI('p'), 'custom', RDF::URI('g')])
+ # end
+ #
# @raise [RDF::LDP::RequestError]
# @raise [RDF::LDP::UnsupportedMediaType] if no reader can be found for the
# graph
#
# @return [RDF::LDP::Resource] self
def update(input, content_type, &block)
- return create(input, content_type) unless exists?
- statements = parse_graph(input, content_type)
- yield statements if block_given?
- graph.clear!
- graph << statements
+ super do |transaction|
+ transaction.graph_name = subject_uri
+ transaction << parse_graph(input, content_type)
+ yield transaction if block_given?
+ graph.clear
+ end
+
self
end
##
# Clears the graph and marks as destroyed.
#
# @see RDF::LDP::Resource#destroy
- def destroy
- @graph.clear
- super
+ def destroy(&block)
+ super do |_|
+ graph.clear
+ end
end
##
- # Returns an Etag. This may be a strong or a weak ETag.
- #
- # @return [String] an HTTP Etag
- #
- # @note the current implementation is a naive one that combines a couple of
- # blunt heurisitics.
- #
- # @todo add an efficient hash function for RDF Graphs to RDF.rb and use that
- # here?
- #
- # @see http://ceur-ws.org/Vol-1259/proceedings.pdf#page=65 for a recent
- # treatment of digests for RDF graphs
- #
- # @see http://www.w3.org/TR/ldp#h-ldpr-gen-etags LDP ETag clause for GET
- # @see http://www.w3.org/TR/ldp#h-ldpr-put-precond LDP ETag clause for PUT
- # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
- # description of strong vs. weak validators
- def etag
- subs = graph.subjects.map { |s| s.node? ? nil : s.to_s }
- .compact.sort.join()
- "\"#{Digest::SHA1.base64digest(subs)}#{graph.statements.count}\""
- end
-
- ##
- # @param [String] tag a tag to compare to `#etag`
- # @return [Boolean] whether the given tag matches `#etag`
- # def match?(tag)
- # return false unless tag.split('==').last == graph.statements.count.to_s
- # end
-
- ##
# @return [Boolean] whether this is an ldp:RDFSource
def rdf_source?
true
end
@@ -159,20 +169,23 @@
private
##
# Process & generate response for PUT requsets.
#
+ # @note patch is currently not transactional.
+ #
# @raise [RDF::LDP::UnsupportedMediaType] when a media type other than
# LDPatch is used
# @raise [RDF::LDP::BadRequest] when an invalid document is given
def patch(status, headers, env)
check_precondition!(env)
method = patch_types[env['CONTENT_TYPE']]
raise UnsupportedMediaType unless method
send(method, env['rack.input'], graph)
+ set_last_modified
[200, update_headers(headers), self]
end
##
# @return [Hash<String,Symbol>] a hash mapping supported PATCH content types
@@ -180,24 +193,20 @@
def patch_types
{ 'text/ldpatch' => :ld_patch,
'application/sparql-update' => :sparql_update }
end
- def ld_patch(input, graph)
- begin
- LD::Patch.parse(input).execute(graph)
- rescue LD::Patch::Error => e
- raise BadRequest, e.message
- end
+ def ld_patch(input, graph, &block)
+ LD::Patch.parse(input).execute(graph)
+ rescue LD::Patch::Error => e
+ raise BadRequest, e.message
end
def sparql_update(input, graph)
- begin
- SPARQL.execute(input, graph, update: true)
- rescue SPARQL::MalformedQuery => e
- raise BadRequest, e.message
- end
+ SPARQL.execute(input, graph, update: true)
+ rescue SPARQL::MalformedQuery => e
+ raise BadRequest, e.message
end
##
# Process & generate response for PUT requsets.
def put(status, headers, env)
@@ -219,11 +228,10 @@
def check_precondition!(env)
raise PreconditionFailed.new('Etag invalid') if
env.has_key?('HTTP_IF_MATCH') && !match?(env['HTTP_IF_MATCH'])
end
-
##
# Finds an {RDF::Reader} appropriate for the given content_type and attempts
# to parse the graph string.
#
# @param [IO, File, String] input a (Rack) input stream IO object or String
@@ -242,10 +250,10 @@
def parse_graph(input, content_type)
reader = RDF::Reader.for(content_type: content_type.to_s)
raise(RDF::LDP::UnsupportedMediaType, content_type) if reader.nil?
input = input.read if input.respond_to? :read
begin
- RDF::Graph.new << reader.new(input, base_uri: subject_uri)
+ RDF::Graph.new << reader.new(input, base_uri: subject_uri, validate: true)
rescue RDF::ReaderError => e
raise RDF::LDP::BadRequest, e.message
end
end
end