lib/rdf/ldp/rdf_source.rb in rdf-ldp-0.2.0 vs lib/rdf/ldp/rdf_source.rb in rdf-ldp-0.3.0
- old
+ new
@@ -1,13 +1,13 @@
-require 'digest/md5'
+require 'digest/sha1'
+require 'ld/patch'
module RDF::LDP
##
# The base class for all directly usable LDP Resources that *are not*
# `NonRDFSources`. RDFSources are implemented as a resource with:
#
- # - a `#subject_uri` identifying the RDFSource (see: {RDF::LDP::Resource}).
# - 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
@@ -17,16 +17,17 @@
#
# 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`.
- # `#metagraph` is invisible to the client unless the implementation mirrors
- # its contents 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
@@ -35,11 +36,13 @@
# @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-rdf-source
def to_uri
RDF::Vocab::LDP.RDFSource
end
end
-
+
+ ##
+ # @see RDF::LDP::Resource#initialize
def initialize(subject_uri, data = RDF::Repository.new)
@graph = RDF::Graph.new(subject_uri, data: data)
super
self
end
@@ -49,10 +52,15 @@
#
# @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.
+ #
# @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
@@ -74,10 +82,19 @@
# @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.
+ #
+ # @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?
@@ -114,47 +131,79 @@
# @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::MD5.base64digest(subs)}#{graph.statements.count}\""
+ "\"#{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
- tag == etag
- end
+ # 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
##
- # @return [RDF::URI] the subject URI for this resource
- def to_uri
- subject_uri
- end
-
- ##
# Returns the graph representing this resource's state, without the graph
# context.
def to_response
RDF::Graph.new << graph
end
private
##
# Process & generate response for PUT requsets.
+ #
+ # @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)
+ [200, update_headers(headers), self]
+ end
+
+ ##
+ # @return [Hash<String,Symbol>] a hash mapping supported PATCH content types
+ # to the method used to process the PATCH request
+ 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
+ end
+
+ def sparql_update(input, graph)
+ begin
+ SPARQL.execute(input, graph, update: true)
+ rescue SPARQL::MalformedQuery => e
+ raise BadRequest, e.message
+ end
+ end
+
+ ##
+ # Process & generate response for PUT requsets.
def put(status, headers, env)
- raise PreconditionFailed.new('Etag invalid') if
- env.has_key?('HTTP_IF_MATCH') && !match?(env['HTTP_IF_MATCH'])
+ check_precondition!(env)
if exists?
update(env['rack.input'], env['CONTENT_TYPE'])
headers = update_headers(headers)
[200, headers, self]
@@ -163,26 +212,40 @@
[201, update_headers(headers), self]
end
end
##
+ # @param [Hash<String, String>] env a rack env
+ # @raise [RDF::LDP::PreconditionFailed]
+ 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] graph an input stream to parse
+ # @param [IO, File, String] input a (Rack) input stream IO object or String
+ # to parse
# @param [#to_s] content_type the content type for the reader
#
# @return [RDF::Enumerable] the statements in the resulting graph
#
# @raise [RDF::LDP::UnsupportedMediaType] if no appropriate reader is found
#
# @todo handle cases where no content type is given? Does RDF::Reader have
# tools to help us here?
- def parse_graph(graph, content_type)
+ #
+ # @see http://www.rubydoc.info/github/rack/rack/file/SPEC#The_Input_Stream
+ # for documentation on input streams in the Rack SPEC
+ 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(graph, base_uri: subject_uri)
+ RDF::Graph.new << reader.new(input, base_uri: subject_uri)
rescue RDF::ReaderError => e
raise RDF::LDP::BadRequest, e.message
end
end
end