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

- old
+ new

@@ -1,35 +1,35 @@ require 'link_header' module RDF::LDP ## - # The base class for all LDP Resources. + # The base class for all LDP Resources. # # The internal state of a Resource is specific to a given persistent datastore # (an `RDF::Repository` passed to the initilazer) and is managed through an # internal graph (`#metagraph`). A Resource has: # # - a `#subject_uri` identifying the Resource. # - a `#metagraph` containing server-internal properties of the Resource. # - # Resources also define a basic set of CRUD operations, identity and current - # state, and a `#to_response`/`#each` method used by Rack & `Rack::LDP` to - # generate an appropriate HTTP response body. + # Resources also define a basic set of CRUD operations, identity and current + # state, and a `#to_response`/`#each` method used by Rack & `Rack::LDP` to + # generate an appropriate HTTP response body. # - # `#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 and a (dcterms:modified) last-modified date. If the - # resource is deleted, a (prov:invalidatedAt) flag in metagraph indicates + # `#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 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 + # + # 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 # @@ -44,28 +44,28 @@ # 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 + # 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 + # @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"=>"", @@ -79,28 +79,28 @@ # @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 + # @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 identifying the `Resource` attr_reader :subject_uri # @!attribute [rw] metagraph # a graph representing the server-internal state of the resource attr_accessor :metagraph - + class << self ## - # @return [RDF::URI] uri with lexical representation + # @return [RDF::URI] uri with lexical representation # 'http://www.w3.org/ns/ldp#Resource' # # @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-resource - def to_uri + def to_uri RDF::Vocab::LDP.Resource end ## # Creates an unique id (URI Slug) for a resource. @@ -111,79 +111,87 @@ def gen_id SecureRandom.uuid end ## - # Finds an existing resource and - # + # Finds an existing resource and + # # @param [RDF::URI] uri the URI for the resource to be found - # @param [RDF::Repository] data a repostiory instance in which to find + # @param [RDF::Repository] data a repostiory instance in which to find # the resource. # # @raise [RDF::LDP::NotFound] when the resource doesn't exist # # @return [RDF::LDP::Resource] a resource instance matching the given URI; - # usually of a subclass + # usually of a subclass # from the interaction models. def find(uri, data) - graph = RDF::Graph.new(uri / '#meta', data: data) + graph = RDF::Graph.new(metagraph_name(uri), data: data) raise NotFound if graph.empty? rdf_class = graph.query([uri, RDF.type, :o]).first klass = INTERACTION_MODELS[rdf_class.object] if rdf_class klass ||= RDFSource - - klass.new(uri, data) + + klass.new(uri, data) end ## # Retrieves the correct interaction model from the Link headers. # # Headers are handled intelligently, e.g. if a client sends a request with - # Resource, RDFSource, and BasicContainer headers, the server gives a - # BasicContainer. An error is thrown if the headers contain conflicting + # Resource, RDFSource, and BasicContainer headers, the server gives a + # BasicContainer. An error is thrown if the headers contain conflicting # types (i.e. NonRDFSource and another Resource class). # - # @param [String] link_header a string containing Link headers from an + # @param [String] link_header a string containing Link headers from an # HTTP request (Rack env) - # - # @return [Class] a subclass of {RDF::LDP::Resource} matching the - # requested interaction model; + # + # @return [Class] a subclass of {RDF::LDP::Resource} matching the + # requested interaction model; def interaction_model(link_header) models = LinkHeader.parse(link_header) .links.select { |link| link['rel'].downcase == 'type' } .map { |link| link.href } return RDFSource if models.empty? match = INTERACTION_MODELS.keys.reverse.find { |u| models.include? u } - + if match == RDF::LDP::NonRDFSource.to_uri - raise NotAcceptable if + raise NotAcceptable if models.include?(RDF::LDP::RDFSource.to_uri) || models.include?(RDF::LDP::Container.to_uri) || models.include?(RDF::LDP::DirectContainer.to_uri) || models.include?(RDF::LDP::IndirectContainer.to_uri) || models.include?(RDF::URI('http://www.w3.org/ns/ldp#BasicContainer')) end INTERACTION_MODELS[match] || RDFSource end + + ## + # Build a graph name URI for the uri passed in + # + # @param uri [RDF::URI] + def metagraph_name(uri) + uri + '#meta' + end end ## # @param [RDF::URI, #to_s] subject_uri the uri that identifies the Resource - # @param [RDF::Repository] data the repository where the resource's RDF - # data (i.e. `metagraph`) is stored; defaults to an in-memory + # @param [RDF::Repository] data the repository where the resource's RDF + # data (i.e. `metagraph`) is stored; defaults to an in-memory # RDF::Repository specific to this Resource. # # @yield [RDF::Resource] Gives itself to the block # - # @example + # @example # RDF::Resource.new('http://example.org/moomin') # # @example with a block - # RDF::Resource.new('http://example.org/moomin') do |resource| + # RDF::Resource.new('http://example.org/moomin') do |resource| # resource.metagraph << RDF::Statement(...) # end # def initialize(subject_uri, data = RDF::Repository.new) @subject_uri = RDF::URI(subject_uri) @@ -193,22 +201,22 @@ end ## # @abstract creates the resource # - # @param [IO, File] input input (usually from a Rack env's + # @param [IO, File] 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 interpret the - # input. This MAY be used as a content type for the created Resource + # 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 + # @raise [RDF::LDP::RequestError] when creation fails. May raise various # subclasses for the appropriate response codes. # @raise [RDF::LDP::Conflict] when the resource exists def create(input, content_type, &block) raise Conflict if exists? @@ -222,21 +230,21 @@ end ## # @abstract update the resource # - # @param [IO, File, #to_s] input input (usually from a Rack env's + # @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 + # @raise [RDF::LDP::RequestError] when update fails. May raise various # subclasses for the appropriate response codes. def update(input, content_type, &block) return create(input, content_type, &block) unless exists? @data.transaction do |transaction| yield transaction if block_given? @@ -246,24 +254,24 @@ end ## # Mark the resource as destroyed. # - # This adds a statment to the metagraph expressing that the resource has + # 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 + # + # @todo Use of owl:Nothing is probably problematic. Define an internal # namespace and class represeting deletion status as a stateful property. def destroy(&block) @data.transaction do |transaction| containers.each { |c| c.remove(self, transaction) if c.container? } - transaction << RDF::Statement(subject_uri, + transaction << RDF::Statement(subject_uri, RDF::Vocab::PROV.invalidatedAtTime, DateTime.now, graph_name: metagraph_name) yield if block_given? end @@ -271,12 +279,12 @@ end ## # Gives the status of the resource's existance. # - # @note destroyed resources continue to exist in the sense represeted by - # this method. + # @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_graph? metagraph.graph_name end @@ -289,40 +297,48 @@ end ## # Returns an Etag. This may be a strong or a weak ETag. # - # @return [String] an HTTP 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 + # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3 # description of strong vs. weak validators def etag return nil unless exists? "W/\"#{last_modified.new_offset(0).iso8601(9)}\"" end ## - # @return [DateTime] the time this resource was last modified + # @return [DateTime] the time this resource was last modified; `nil` if the + # resource doesn't exist and has no modified date + # @raise [RDF::LDP::RequestError] when the resource exists but is missing a + # `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? + + if results.empty? + return nil unless exists? + raise(RequestError, "Missing dc:modified date for #{subject_uri}") + end + 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 + tag == etag end ## # @return [RDF::URI] the subject URI for this resource def to_uri @@ -330,11 +346,11 @@ end ## # @return [Array<Symbol>] a list of HTTP methods allowed by this resource. def allowed_methods - [:GET, :POST, :PUT, :DELETE, :PATCH, :OPTIONS, :HEAD].select do |m| + [:GET, :POST, :PUT, :DELETE, :PATCH, :OPTIONS, :HEAD].select do |m| respond_to?(m.downcase, true) end end ## @@ -368,71 +384,71 @@ RDF::LDP::Resource.find(st.subject, @data) end end ## - # Runs the request and returns the object's desired HTTP response body, - # conforming to the Rack interfare. + # Runs the request and returns the object's desired HTTP response body, + # conforming to the Rack interfare. # - # @see http://www.rubydoc.info/github/rack/rack/master/file/SPEC#The_Body + # @see http://www.rubydoc.info/github/rack/rack/master/file/SPEC#The_Body # for Rack body documentation def to_response [] end alias_method :each, :to_response ## # Build the response for the HTTP `method` given. - # + # # The method passed in is symbolized, downcased, and sent to `self` with the # other three parameters. # # Request methods are expected to return an Array appropriate for a Rack - # response; to return this object (e.g. for a sucessful GET) the response + # response; to return this object (e.g. for a sucessful GET) the response # may be `[status, headers, self]`. # - # If the method given is unimplemented, we understand it to require an HTTP + # If the method given is unimplemented, we understand it to require an HTTP # 405 response, and throw the appropriate error. # - # @param [#to_sym] method the HTTP request method of the response; this + # @param [#to_sym] method the HTTP request method of the response; this # message will be downcased and sent to the object. - # @param [Fixnum] status an HTTP response code; this status should be sent + # @param [Fixnum] status an HTTP response code; this status should be sent # back to the caller or altered, as appropriate. - # @param [Hash<String, String>] headers a hash mapping HTTP headers - # built for the response to their contents; these headers should be sent + # @param [Hash<String, String>] headers a hash mapping HTTP headers + # built for the response to their contents; these headers should be sent # back to the caller or altered, as appropriate. # @param [Hash] env the Rack env for the request # - # @return [Array<Fixnum, Hash<String, String>, #each] a new Rack response + # @return [Array<Fixnum, Hash<String, String>, #each] a new Rack response # array. def request(method, status, headers, env) raise Gone if destroyed? begin send(method.to_sym.downcase, status, headers, env) rescue NotImplementedError => e - raise MethodNotAllowed, method + raise MethodNotAllowed, method end end private ## - # Generate response for GET requests. Returns existing status and headers, + # Generate response for GET requests. Returns existing status and headers, # with `self` as the body. def get(status, headers, env) [status, update_headers(headers), self] end ## - # Generate response for HEAD requsets. Adds appropriate headers and returns + # Generate response for HEAD requsets. Adds appropriate headers and returns # an empty body. def head(status, headers, env) [status, update_headers(headers), []] end ## - # Generate response for OPTIONS requsets. Adds appropriate headers and + # Generate response for OPTIONS requsets. Adds appropriate headers and # returns an empty body. def options(status, headers, env) [status, update_headers(headers), []] end @@ -473,20 +489,20 @@ end ## # @return [RDF::URI] the name for this resource's metagraph def metagraph_name - subject_uri / '#meta' + self.class.metagraph_name(subject_uri) end ## # @param [Hash<String, String>] headers # @return [Hash<String, String>] the updated headers def update_headers(headers) - headers['Link'] = + headers['Link'] = ([headers['Link']] + link_headers).compact.join(",") - + 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) tag = etag @@ -509,11 +525,11 @@ def accept_patch respond_to?(:patch_types, true) ? patch_types.keys.join(',') : '' end ## - # @return [Array<String>] an array of link headers to add to the + # @return [Array<String>] an array of link headers to add to the # existing ones # # @see http://www.w3.org/TR/ldp/#h-ldpr-gen-linktypehdr # @see http://www.w3.org/TR/ldp/#h-ldprs-are-ldpr # @see http://www.w3.org/TR/ldp/#h-ldpnr-type @@ -540,28 +556,28 @@ 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, + 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, + 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 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, + transaction.insert(RDF::Statement(subject_uri, + RDF.type, self.class.to_uri, graph_name: metagraph.graph_name)) end end end