lib/rdf/ldp/non_rdf_source.rb in rdf-ldp-0.2.0 vs lib/rdf/ldp/non_rdf_source.rb in rdf-ldp-0.3.0

- old
+ new

@@ -1,7 +1,27 @@ module RDF::LDP + ## + # A NonRDFSource describes a `Resource` whose response body is a format other + # than an RDF serialization. The persistent state of the resource, as + # represented by the body, is persisted to an IO stream provided by a + # `RDF::LDP::NonRDFSource::StorageAdapter` given by `#storage`. + # + # In addition to the properties stored by the `RDF::LDP::Resource#metagraph`, + # `NonRDFSource`s also store a content type (format). + # + # When a `NonRDFSource` is created, it also creates an `RDFSource` which + # describes it. This resource is created at the URI in `#description_uri`, + # the resource itself is returned by `#description`. + # + # @see RDF::LDP::Resource + # @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-non-rdf-source for + # a definition of NonRDFSource in LDP class NonRDFSource < Resource + # Use DC elements format + FORMAT_TERM = RDF::DC11.format + DESCRIBED_BY_TERM = RDF::URI('http://www.w3.org/2007/05/powder-s#describedby') + ## # @return [RDF::URI] uri with lexical representation # 'http://www.w3.org/ns/ldp#NonRDFSource' # # @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-non-rdf-source @@ -11,8 +31,225 @@ ## # @return [Boolean] whether this is an ldp:NonRDFSource def non_rdf_source? true + end + + ## + # @param [IO, File] input input (usually from a Rack env's + # `rack.input` key) that will be read into the NonRDFSource + # @param [#to_s] c_type a MIME content_type used as a content type + # for the created NonRDFSource + # + # @raise [RDF::LDP::RequestError] when saving the NonRDFSource + # + # @return [RDF::LDP::NonRDFSource] self + # + # @see RDF::LDP::Resource#create + def create(input, c_type) + storage.io { |io| IO.copy_stream(input.binmode, io) } + super + self.content_type = c_type + RDFSource.new(description_uri, @data).create('', 'text/plain') + self + end + + ## + # @see RDF::LDP::Resource#update + def update(input, c_type) + storage.io { |io| IO.copy_stream(input.binmode, io) } + self.content_type = c_type + self + end + + ## + # Deletes the LDP-NR contents from the storage medium and marks the + # resource as destroyed. + # + # @see RDF::LDP::Resource#destroy + def destroy + storage.delete + super + end + + def etag + "#{Digest::SHA1.base64digest(storage.io.read)}" + end + + ## + # @raise [RDF::LDP::NotFound] if the describedby resource doesn't exist + # + # @return [RDF::LDP::RDFSource] resource describing this resource + def description + RDF::LDP::Resource.find(description_uri, @data) + end + + ## + # @return [RDF::URI] uri for this resource's associated RDFSource + def description_uri + subject_uri / '.well-known' / 'desc' + end + + ## + # @return [StorageAdapter] the storage adapter for this LDP-NR + def storage + @storage_adapter ||= StorageAdapter.new(self) + end + + ## + # Sets the MIME type for the resource in `metagraph`. + # + # @param [String] a string representing the content type for this LDP-NR. + # This SHOULD be a regisered MIME type. + # + # @return [StorageAdapter] the content type + def content_type=(content_type) + metagraph.delete([subject_uri, FORMAT_TERM]) + metagraph << RDF::Statement(subject_uri, RDF::DC11.format, content_type) + end + + ## + # @return [StorageAdapter] this resource's content type + def content_type + format_triple = metagraph.first([subject_uri, FORMAT_TERM, :format]) + format_triple.nil? ? nil : format_triple.object.object + end + + ## + # @return [#each] the response body. This is normally the StorageAdapter's + # IO object in read and binary mode. + # + # @raise [RDF::LDP::RequestError] when the request fails + def to_response + (exists? && !destroyed?) ? storage.io : [] + end + + private + + ## + # 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']) + + if exists? + update(env['rack.input'], env['CONTENT_TYPE']) + headers = update_headers(headers) + [200, headers, self] + else + create(env['rack.input'], env['CONTENT_TYPE']) + [201, update_headers(headers), self] + end + end + + ## + # @see RDF::LDP::Resource#update_headers + def update_headers(headers) + headers['Content-Type'] = content_type + super + end + + def link_headers + super << "<#{description_uri}>;rel=\"describedBy\"" + end + + ## + # StorageAdapters bundle the logic for mapping a `NonRDFSource` to a + # specific IO stream. Implementations must conform to a minimal interface: + # + # - `#initailize` must accept a `resource` parameter. The input should be + # a `NonRDFSource` (LDP-NR). + # - `#io` must yield and return a IO object in binary mode that represents + # the current state of the LDP-NR. + # - If a block is passed to `#io`, the implementation MUST allow return a + # writable IO object and that anything written to the stream while + # yielding is synced with the source in a thread-safe manner. + # - Clients not passing a block to `#io` SHOULD call `#close` on the + # object after reading it. + # - If the `#io` object responds to `#to_path` it MUST give the location + # of a file whose contents are identical the IO object's. This supports + # Rack's response body interface. + # - `#delete` remove the contents from the corresponding storage. This MAY + # be a no-op if is undesirable or impossible to delete the contents + # from the storage medium. + # + # @see http://www.rubydoc.info/github/rack/rack/master/file/SPEC#The_Body + # for details about `#to_path` in Rack response bodies. + # + # @example reading from a `StorageAdapter` + # storage = StorageAdapter.new(an_nr_source) + # storage.io.read # => [string contents of `an_nr_source`] + # + # @example writing to a `StorageAdapter` + # storage = StorageAdapter.new(an_nr_source) + # storage.io { |io| io.write('moomin') + # + # Beyond this interface, implementations are permitted to behave as desired. + # They may, for instance, reject undesirable content or alter the graph (or + # metagraph) of the resource. They should throw appropriate `RDF::LDP` + # errors when failing to allow the middleware to handle response codes and + # messages. + # + # The base storage adapter class provides a simple File storage + # implementation. + # + # @todo check thread saftey on write for base implementation + class StorageAdapter + STORAGE_PATH = '.storage'.freeze + + ## + # Initializes the storage adapter. + # + # @param [NonRDFSource] resource + def initialize(resource) + @resource = resource + end + + ## + # Gives an IO object which represents the current state of @resource. + # Opens the file for read-write (mode: r+), if it already exists; + # otherwise, creates the file and opens it for read-write (mode: w+). + # + # @yield [IO] yields a read-writable object conforming to the Ruby IO + # interface for storage. The IO object will be closed when the block + # ends. + # + # @return [IO] an object conforming to the Ruby IO interface + def io(&block) + FileUtils.mkdir_p(path_dir) unless Dir.exists?(path_dir) + FileUtils.touch(path) unless file_exists? + + File.open(path, 'r+b', &block) + end + + ## + # @return [Boolean] 1 if the file has been deleted, otherwise false + def delete + return false unless File.exists?(path) + File.delete(path) + end + + private + + ## + # @return [Boolean] + def file_exists? + File.exists?(path) + end + + ## + # Build the path to the file on disk. + # @return [String] + def path + File.join(STORAGE_PATH, @resource.subject_uri.path) + end + + ## + # Build the path to the file's directory on disk + # @return [String] + def path_dir + File.split(path).first + end end end end