class RDF::LDP::Resource

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.

`#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 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(StringIO.new(''), 'text/plain')

resource.exists? # => true
resource.metagraph.dump :ttl
# => "<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"^^xsd:dateTime ."

@example updating a Resource updates the `#last_modified` date

resource.last_modified
# => #<DateTime: 2015-10-25T14:32:01-07:00...>
resource.update('blah', 'text/plain')
resource.last_modified
# => #<DateTime: 2015-10-25T14:32:04-07:00...>

@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:0xea7(http://example.org/moomin#meta)>,
        @subject_uri=#<RDF::URI:0xea8 URI:http://example.org/moomin>>]

resource.request(:put, 200, {}, {}) # RDF::LDP::MethodNotAllowed: put

@see www.w3.org/TR/ldp/ Linked Data platform Specification @see www.w3.org/TR/ldp/#dfn-linked-data-platform-resource Definition

of 'Resource' in LDP

Constants

CONTAINS_URI
INVALIDATED_AT_URI
MODIFIED_URI

Attributes

metagraph[RW]

@!attribute [rw] metagraph

a graph representing the server-internal state of the resource
subject_uri[R]

@!attribute [r] subject_uri

an rdf term identifying the `Resource`

Public Class Methods

find(uri, data) click to toggle source

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

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
from the interaction models.
# File lib/rdf/ldp/resource.rb, line 132
def find(uri, data)
  graph = RDF::Graph.new(graph_name: metagraph_name(uri), data: data)
  raise NotFound if graph.empty?

  klass = graph.query([uri, RDF.type, :o]).find do |rdf_class|
    candidate = InteractionModel.for(rdf_class.object)
    break candidate unless candidate.nil?
  end
  klass ||= RDFSource

  klass.new(uri, data)
end
gen_id() click to toggle source

Creates an unique id (URI Slug) for a resource.

@note the current implementation uses {SecureRandom#uuid}.

@return [String] a unique ID

# File lib/rdf/ldp/resource.rb, line 116
def gen_id
  SecureRandom.uuid
end
interaction_model(link_header) click to toggle source

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 types (i.e. NonRDFSource and another Resource class).

@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;
# File lib/rdf/ldp/resource.rb, line 158
def interaction_model(link_header)
  models =
    LinkHeader.parse(link_header)
              .links.select { |link| link['rel'].casecmp 'type' }
              .map { |link| RDF::URI.intern(link.href) }

  return InteractionModel.default if models.empty?

  raise NotAcceptable unless InteractionModel.compatible?(models)

  InteractionModel.find(models)
end
metagraph_name(uri) click to toggle source

Build a graph name URI for the uri passed in

@param uri [RDF::URI]

# File lib/rdf/ldp/resource.rb, line 175
def metagraph_name(uri)
  uri + '#meta'
end
new(subject_uri, data = RDF::Repository.new) { |self| ... } click to toggle source

@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
RDF::Repository specific to this Resource.

@yield [RDF::Resource] Gives itself to the block

@example

RDF::Resource.new('http://example.org/moomin')

@example with a block

RDF::Resource.new('http://example.org/moomin') do |resource|
  resource.metagraph << RDF::Statement(...)
end
# File lib/rdf/ldp/resource.rb, line 196
def initialize(subject_uri, data = RDF::Repository.new)
  @subject_uri = RDF::URI.intern(subject_uri)
  @data = data
  @metagraph = RDF::Graph.new(graph_name: metagraph_name, data: data)
  yield self if block_given?
end
to_uri() click to toggle source

@return [RDF::URI] uri with lexical representation

'http://www.w3.org/ns/ldp#Resource'

@see www.w3.org/TR/ldp/#dfn-linked-data-platform-resource

# File lib/rdf/ldp/resource.rb, line 106
def to_uri
  RDF::Vocab::LDP.Resource
end

Public Instance Methods

allowed_methods() click to toggle source

@return [Array<Symbol>] a list of HTTP methods allowed by this resource.

# File lib/rdf/ldp/resource.rb, line 354
def allowed_methods
  [:GET, :POST, :PUT, :DELETE, :PATCH, :OPTIONS, :HEAD].select do |m|
    respond_to?(m.downcase, true)
  end
end
container?() click to toggle source

@return [Boolean] whether this is an ldp:Container

# File lib/rdf/ldp/resource.rb, line 368
def container?
  false
end
containers() click to toggle source

@return [Array<RDF::LDP::Resource>] the container for this resource

# File lib/rdf/ldp/resource.rb, line 386
def containers
  @data.query([:s, CONTAINS_URI, subject_uri]).map do |st|
    RDF::LDP::Resource.find(st.subject, @data)
  end
end
create(_input, _content_type) { |transaction| ... } click to toggle source

@abstract creates the resource

@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
(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

# File lib/rdf/ldp/resource.rb, line 220
def create(_input, _content_type)
  raise Conflict if exists?

  @data.transaction(mutable: true) do |transaction|
    set_interaction_model(transaction)
    yield transaction if block_given?
    set_last_modified(transaction)
  end

  self
end
destroy() { |transaction| ... } click to toggle source

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.

# File lib/rdf/ldp/resource.rb, line 271
def destroy
  @data.transaction(mutable: true) do |transaction|
    containers.each { |c| c.remove(self, transaction) if c.container? }
    transaction.insert RDF::Statement(subject_uri,
                                      INVALIDATED_AT_URI,
                                      DateTime.now,
                                      graph_name: metagraph_name)
    yield transaction if block_given?
  end
  self
end
destroyed?() click to toggle source

@return [Boolean] true if resource has been destroyed

# File lib/rdf/ldp/resource.rb, line 296
def destroyed?
  times = @metagraph.query([subject_uri, INVALIDATED_AT_URI, nil])
  !times.empty?
end
each()
Alias for: to_response
etag() click to toggle source

Returns an Etag. This may be a strong or a weak ETag.

@return [String] an HTTP Etag

@note these etags are weak, but we allow clients to use them in

`If-Match` headers, and use weak comparison. This is in conflict with
https://tools.ietf.org/html/rfc7232#section-3.1. See:
https://github.com/ruby-rdf/rdf-ldp/issues/68

@see www.w3.org/TR/ldp#h-ldpr-gen-etags LDP ETag clause for GET @see www.w3.org/TR/ldp#h-ldpr-put-precond LDP ETag clause for PUT @see tools.ietf.org/html/rfc7232#section-2.1

Weak vs. strong validators
# File lib/rdf/ldp/resource.rb, line 315
def etag
  return nil unless exists?
  "W/\"#{last_modified.new_offset(0).iso8601(9)}\""
end
exists?() click to toggle source

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

# File lib/rdf/ldp/resource.rb, line 290
def exists?
  @data.has_graph? metagraph.graph_name
end
last_modified() click to toggle source

@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
# File lib/rdf/ldp/resource.rb, line 328
def last_modified
  results = @metagraph.query([subject_uri, RDF::Vocab::DC.modified, :time])

  if results.empty?
    return nil unless exists?
    raise(RequestError, "Missing dc:modified date for #{subject_uri}")
  end

  results.first.object.object
end
ldp_resource?() click to toggle source

@return [Boolean] whether this is an ldp:Resource

# File lib/rdf/ldp/resource.rb, line 362
def ldp_resource?
  true
end
match?(tag) click to toggle source

@param [String] tag a tag to compare to `#etag` @return [Boolean] whether the given tag matches `#etag`

# File lib/rdf/ldp/resource.rb, line 342
def match?(tag)
  tag == etag
end
non_rdf_source?() click to toggle source

@return [Boolean] whether this is an ldp:NonRDFSource

# File lib/rdf/ldp/resource.rb, line 374
def non_rdf_source?
  false
end
rdf_source?() click to toggle source

@return [Boolean] whether this is an ldp:RDFSource

# File lib/rdf/ldp/resource.rb, line 380
def rdf_source?
  false
end
request(method, status, headers, env) click to toggle source

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 may be `[status, headers, self]`.

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

message will be downcased and sent to the object.

@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
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

array.
# File lib/rdf/ldp/resource.rb, line 427
def request(method, status, headers, env)
  raise Gone if destroyed?
  begin
    send(method.to_sym.downcase, status, headers, env)
  rescue NotImplementedError
    raise MethodNotAllowed, method
  end
end
to_response() click to toggle source

Runs the request and returns the object's desired HTTP response body, conforming to the Rack interfare.

@see www.rubydoc.info/github/rack/rack/master/file/SPEC#The_Body

Rack body documentation
# File lib/rdf/ldp/resource.rb, line 398
def to_response
  []
end
Also aliased as: each
to_uri() click to toggle source

@return [RDF::URI] the subject URI for this resource

# File lib/rdf/ldp/resource.rb, line 348
def to_uri
  subject_uri
end
update(input, content_type) { |transaction| ... } click to toggle source

@abstract update the resource

@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.
# File lib/rdf/ldp/resource.rb, line 247
def update(input, content_type, &block)
  return create(input, content_type, &block) unless exists?

  @data.transaction(mutable: true) do |transaction|
    yield transaction if block_given?
    set_last_modified(transaction)
  end

  self
end

Private Instance Methods

accept_patch() click to toggle source

@return [String] the Accept-Patch headers

# File lib/rdf/ldp/resource.rb, line 531
def accept_patch
  respond_to?(:patch_types, true) ? patch_types.keys.join(',') : ''
end
accept_post() click to toggle source

@return [String] the Accept-Post headers

# File lib/rdf/ldp/resource.rb, line 525
def accept_post
  RDF::Reader.map(&:format).compact.map(&:content_type).flatten.join(', ')
end
connect(*) click to toggle source

@abstract HTTP CONNECT is not expected to be supported

# File lib/rdf/ldp/resource.rb, line 493
def connect(*)
  raise NotImplementedError
end
delete(_status, headers, _env) click to toggle source

Process & generate response for DELETE requests.

# File lib/rdf/ldp/resource.rb, line 461
def delete(_status, headers, _env)
  destroy
  headers.delete('Content-Type')
  [204, headers, []]
end
get(status, headers, _env) click to toggle source

Generate response for GET requests. Returns existing status and headers, with `self` as the body.

# File lib/rdf/ldp/resource.rb, line 441
def get(status, headers, _env)
  [status, update_headers(headers), self]
end
head(status, headers, _env) click to toggle source

Generate response for HEAD requsets. Adds appropriate headers and returns an empty body.

# File lib/rdf/ldp/resource.rb, line 448
def head(status, headers, _env)
  [status, update_headers(headers), []]
end
metagraph_name() click to toggle source

@return [RDF::URI] the name for this resource's metagraph

# File lib/rdf/ldp/resource.rb, line 499
def metagraph_name
  self.class.metagraph_name(subject_uri)
end
options(status, headers, _env) click to toggle source

Generate response for OPTIONS requsets. Adds appropriate headers and returns an empty body.

# File lib/rdf/ldp/resource.rb, line 455
def options(status, headers, _env)
  [status, update_headers(headers), []]
end
patch(*) click to toggle source

@abstract implement in subclasses as needed to support HTTP PATCH

# File lib/rdf/ldp/resource.rb, line 469
def patch(*)
  raise NotImplementedError
end
post(*) click to toggle source

@abstract implement in subclasses as needed to support HTTP POST

# File lib/rdf/ldp/resource.rb, line 475
def post(*)
  raise NotImplementedError
end
put(*) click to toggle source

@abstract implement in subclasses as needed to support HTTP PUT

# File lib/rdf/ldp/resource.rb, line 481
def put(*)
  raise NotImplementedError
end
set_interaction_model(transaction) click to toggle source

Sets the interaction model to the URI for this resource's class

# File lib/rdf/ldp/resource.rb, line 585
def set_interaction_model(transaction)
  transaction.insert(RDF::Statement(subject_uri,
                                    RDF.type,
                                    self.class.to_uri,
                                    graph_name: metagraph.graph_name))
end
set_last_modified(transaction = nil) click to toggle source

Sets the last modified date/time to now

@param transaction [RDF::Transaction] the transaction scope in which to

apply changes. If none (or `nil`) is given, the change is made outside
any transaction scope.
# File lib/rdf/ldp/resource.rb, line 565
def set_last_modified(transaction = nil)
  return metagraph.update([subject_uri, MODIFIED_URI, DateTime.now]) unless
    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
  if last_modified
    transaction
      .delete RDF::Statement(subject_uri, MODIFIED_URI, last_modified,
                             graph_name: metagraph_name)
  end

  transaction
    .insert RDF::Statement(subject_uri, MODIFIED_URI, DateTime.now,
                           graph_name: metagraph_name)
end
trace(*) click to toggle source

@abstract HTTP TRACE is not expected to be supported

# File lib/rdf/ldp/resource.rb, line 487
def trace(*)
  raise NotImplementedError
end
update_headers(headers) click to toggle source

@param [Hash<String, String>] headers @return [Hash<String, String>] the updated headers

# File lib/rdf/ldp/resource.rb, line 506
def update_headers(headers)
  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
  headers['ETag'] ||= tag if tag

  modified = last_modified
  headers['Last-Modified'] ||= modified.httpdate if modified

  headers
end