lib/rdf/ldp/resource.rb in rdf-ldp-0.3.0 vs lib/rdf/ldp/resource.rb in rdf-ldp-0.4.0

- old
+ new

@@ -17,35 +17,77 @@ # # `#metagraph' holds internal properites used by the server. It is distinct # from, and may conflict with, other RDF and non-RDF information about the # resource (e.g. representations suitable for a response body). Metagraph # contains a canonical `rdf:type` statement, which specifies the resource's - # interaction model. If the resource is deleted, a flag in metagraph - # indicates this. + # interaction model and a (dcterms:modified) last-modified date. If the + # resource is deleted, a (prov:invalidatedAt) flag in metagraph indicates + # this. # # The contents of `#metagraph` should not be confused with LDP # server-managed-triples, Those triples are included in the state of the # resource as represented by the response body. `#metagraph` is invisible to # the client except where a subclass mirrors its contents in the body. - # + # # @example creating a new Resource # repository = RDF::Repository.new # resource = RDF::LDP::Resource.new('http://example.org/moomin', repository) # resource.exists? # => false # # resource.create('', 'text/plain') # # resource.exists? # => true # resource.metagraph.dump :ttl - # # => "<http://example.org/moomin> a <http://www.w3.org/ns/ldp#Resource> ." + # # => "<http://example.org/moomin> a <http://www.w3.org/ns/ldp#Resource>; + # <http://purl.org/dc/terms/modified> "2015-10-25T14:24:56-07:00"^^<http://www.w3.org/2001/XMLSchema#dateTime> ." # + # @example updating a Resource updates the `#last_modified` date + # resource.last_modified + # # => #<DateTime: 2015-10-25T14:32:01-07:00 ((2457321j,77521s,571858283n),-25200s,2299161j)> + # resource.update('blah', 'text/plain') + # resource.last_modified + # # => #<DateTime: 2015-10-25T14:32:04-07:00 ((2457321j,77524s,330658065n),-25200s,2299161j)> + # + # @example destroying a Resource + # resource.exists? # => true + # resource.destroyed? # => false + # + # resource.destroy + # + # resource.exists? # => true + # resource.destroyed? # => true + # + # Rack (via `RDF::LDP::Rack`) uses the `#request` method to dispatch requests and + # interpret responses. Disallowed HTTP methods result in + # `RDF::LDP::MethodNotAllowed`. Individual Resources populate `Link`, `Allow`, + # `ETag`, `Last-Modified`, and `Accept-*` headers as required by LDP. All + # subclasses (MUST) return `self` as the Body, and respond to `#each`/ + # `#respond_to` with the intended body. + # + # @example using HTTP request methods to get a Rack response + # resource.request(:get, 200, {}, {}) + # # => [200, + # {"Link"=>"<http://www.w3.org/ns/ldp#Resource>;rel=\"type\"", + # "Allow"=>"GET, DELETE, OPTIONS, HEAD", + # "Accept-Post"=>"", + # "Accept-Patch"=>"", + # "ETag"=>"W/\"2015-10-25T21:39:13.111500405+00:00\"", + # "Last-Modified"=>"Sun, 25 Oct 2015 21:39:13 GMT"}, + # #<RDF::LDP::Resource:0x00564f4a646028 + # @data=#<RDF::Repository:0x2b27a5391708()>, + # @exists=true, + # @metagraph=#<RDF::Graph:0x2b27a5322538(http://example.org/moomin#meta)>, + # @subject_uri=#<RDF::URI:0x2b27a5322fec URI:http://example.org/moomin>>] + # + # resource.request(:put, 200, {}, {}) # RDF::LDP::MethodNotAllowed: put + # # @see http://www.w3.org/TR/ldp/ for the Linked Data platform specification # @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-resource for a # definition of 'Resource' in LDP class Resource # @!attribute [r] subject_uri - # an rdf term + # an rdf term identifying the `Resource` attr_reader :subject_uri # @!attribute [rw] metagraph # a graph representing the server-internal state of the resource attr_accessor :metagraph @@ -157,18 +199,27 @@ # `rack.input` key) used to determine the Resource's initial state. # @param [#to_s] content_type a MIME content_type used to interpret the # input. This MAY be used as a content type for the created Resource # (especially for `LDP::NonRDFSource`s). # + # @yield gives a transaction (changeset) to collect changes to graph, + # metagraph and other resources' (e.g. containers) graphs + # @yieldparam tx [RDF::Transaction] + # @return [RDF::LDP::Resource] self + # # @raise [RDF::LDP::RequestError] when creation fails. May raise various # subclasses for the appropriate response codes. # @raise [RDF::LDP::Conflict] when the resource exists - # - # @return [RDF::LDP::Resource] self - def create(input, content_type) + def create(input, content_type, &block) raise Conflict if exists? - metagraph << RDF::Statement(subject_uri, RDF.type, self.class.to_uri) + + @data.transaction do |transaction| + set_interaction_model(transaction) + yield transaction if block_given? + set_last_modified(transaction) + end + self end ## # @abstract update the resource @@ -176,51 +227,98 @@ # @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 a transaction (changeset) to collect changes to graph, + # metagraph and other resources' (e.g. containers) graphs + # @yieldparam tx [RDF::Transaction] + # @return [RDF::LDP::Resource] self + # # @raise [RDF::LDP::RequestError] when update fails. May raise various # subclasses for the appropriate response codes. - # - # @return [RDF::LDP::Resource] self - def update(input, content_type) - raise NotImplementedError + def update(input, content_type, &block) + return create(input, content_type, &block) unless exists? + @data.transaction do |transaction| + yield transaction if block_given? + set_last_modified(transaction) + end + self end ## # Mark the resource as destroyed. # # This adds a statment to the metagraph expressing that the resource has # been deleted # + # @yield gives a transaction (changeset) to collect changes to graph, + # metagraph and other resources' (e.g. containers) graphs + # @yieldparam tx [RDF::Transaction] # @return [RDF::LDP::Resource] self # # @todo Use of owl:Nothing is probably problematic. Define an internal # namespace and class represeting deletion status as a stateful property. - def destroy - containers.each { |con| con.remove(self) if con.container? } - @metagraph << RDF::Statement(subject_uri, RDF.type, RDF::OWL.Nothing) + def destroy(&block) + @data.transaction do |transaction| + containers.each { |c| c.remove(self, transaction) if c.container? } + transaction << RDF::Statement(subject_uri, + RDF::Vocab::PROV.invalidatedAtTime, + DateTime.now, + graph_name: metagraph_name) + yield if block_given? + end self end ## + # Gives the status of the resource's existance. + # + # @note destroyed resources continue to exist in the sense represeted by + # this method. + # # @return [Boolean] true if the resource exists within the repository def exists? - @data.has_context? metagraph.context + @data.has_graph? metagraph.graph_name end ## # @return [Boolean] true if resource has been destroyed def destroyed? - !(@metagraph.query([subject_uri, RDF.type, RDF::OWL.Nothing]).empty?) + times = @metagraph.query([subject_uri, RDF::Vocab::PROV.invalidatedAtTime, nil]) + !(times.empty?) end + ## + # Returns an Etag. This may be a strong or a weak ETag. + # + # @return [String] an HTTP Etag + # + # @note these etags are strong if (and only if) all software that updates + # the resource also updates the ETag + # + # @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 - nil + return nil unless exists? + "W/\"#{last_modified.new_offset(0).iso8601(9)}\"" end ## + # @return [DateTime] the time this resource was last modified + # + # @todo handle cases where there is more than one RDF::DC.modified. + # check for the most recent date + def last_modified + results = @metagraph.query([subject_uri, RDF::Vocab::DC.modified, :time]) + return nil if results.empty? + results.first.object.object + end + + ## # @param [String] tag a tag to compare to `#etag` # @return [Boolean] whether the given tag matches `#etag` def match?(tag) tag == etag end @@ -308,11 +406,11 @@ # array. def request(method, status, headers, env) raise Gone if destroyed? begin send(method.to_sym.downcase, status, headers, env) - rescue NotImplementedError, NoMethodError => e + rescue NotImplementedError => e raise MethodNotAllowed, method end end private @@ -343,10 +441,40 @@ def delete(status, headers, env) [204, headers, destroy] end ## + # @abstract implement in subclasses as needed to support HTTP PATCH + def patch(*) + raise NotImplementedError + end + + ## + # @abstract implement in subclasses as needed to support HTTP POST + def post(*) + raise NotImplementedError + end + + ## + # @abstract implement in subclasses as needed to support HTTP PUT + def put(*) + raise NotImplementedError + end + + ## + # @abstract HTTP TRACE is not expected to be supported + def trace(*) + raise NotImplementedError + end + + ## + # @abstract HTTP CONNECT is not expected to be supported + def connect(*) + raise NotImplementedError + end + + ## # @return [RDF::URI] the name for this resource's metagraph def metagraph_name subject_uri / '#meta' end @@ -359,18 +487,23 @@ headers['Allow'] = allowed_methods.join(', ') headers['Accept-Post'] = accept_post if respond_to?(:post, true) headers['Accept-Patch'] = accept_patch if respond_to?(:patch, true) - headers['ETag'] ||= etag if etag + tag = etag + headers['ETag'] ||= tag if tag + + modified = last_modified + headers['Last-Modified'] ||= modified.httpdate if modified + headers end ## # @return [String] the Accept-Post headers def accept_post - RDF::Reader.map { |klass| klass.format.content_type }.flatten.join(', ') + RDF::Reader.map(&:format).compact.map(&:content_type).flatten.join(', ') end ## # @return [String] the Accept-Patch headers def accept_patch @@ -397,8 +530,39 @@ ## # @return [String] a string to insert into a Link header def link_type_header(uri) "<#{uri}>;rel=\"type\"" + end + + ## + # Sets the last modified date/time to now + def set_last_modified(transaction = nil) + if transaction + # transactions do not support updates or pattern deletes, so we must + # ask the Repository for the current last_modified to delete the statement + # transactionally + modified = last_modified + transaction.delete RDF::Statement(subject_uri, + RDF::Vocab::DC.modified, + modified, + graph_name: metagraph_name) if modified + + transaction.insert RDF::Statement(subject_uri, + RDF::Vocab::DC.modified, + DateTime.now, + graph_name: metagraph_name) + else + metagraph.update([subject_uri, RDF::Vocab::DC.modified, DateTime.now]) + end + end + + ## + # Sets the last modified date/time to the URI for this resource's class + def set_interaction_model(transaction) + transaction.insert(RDF::Statement(subject_uri, + RDF.type, + self.class.to_uri, + graph_name: metagraph.graph_name)) end end end