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