Class: RDF::LDP::Resource

Inherits:
Object
  • Object
show all
Defined in:
lib/rdf/ldp/resource.rb

Overview

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.

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.

Examples:

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"^^<http://www.w3.org/2001/XMLSchema#dateTime> ."

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

destroying a Resource

resource.exists? # => true
resource.destroyed? # => false

resource.destroy

resource.exists? # => true
resource.destroyed? # => true

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 Also:

Direct Known Subclasses

NonRDFSource, RDFSource

Constant Summary collapse

CONTAINS_URI =
RDF::Vocab::LDP.contains.freeze
INVALIDATED_AT_URI =
RDF::Vocab::PROV.invalidatedAtTime.freeze
MODIFIED_URI =
RDF::Vocab::DC.modified.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(subject_uri, data = RDF::Repository.new) {|RDF::Resource| ... } ⇒ Resource

Returns a new instance of Resource.

Examples:

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

with a block

RDF::Resource.new('http://example.org/moomin') do |resource|
  resource.metagraph << RDF::Statement(...)
end

Parameters:

  • subject_uri (RDF::URI, #to_s)

    the uri that identifies the Resource

  • data (RDF::Repository) (defaults to: RDF::Repository.new)

    the repository where the resource’s RDF data (i.e. ‘metagraph`) is stored; defaults to an in-memory RDF::Repository specific to this Resource.

Yields:

  • (RDF::Resource)

    Gives itself to the block



201
202
203
204
205
206
# File 'lib/rdf/ldp/resource.rb', line 201

def initialize(subject_uri, data = RDF::Repository.new)
  @subject_uri = RDF::URI(subject_uri)
  @data = data
  @metagraph = RDF::Graph.new(graph_name: metagraph_name, data: data)
  yield self if block_given?
end

Instance Attribute Details

#metagraphObject

a graph representing the server-internal state of the resource



97
98
99
# File 'lib/rdf/ldp/resource.rb', line 97

def metagraph
  @metagraph
end

#subject_uriObject (readonly)

Returns the value of attribute subject_uri.



93
94
95
# File 'lib/rdf/ldp/resource.rb', line 93

def subject_uri
  @subject_uri
end

Class Method Details

.find(uri, data) ⇒ RDF::LDP::Resource

Finds an existing resource and

Parameters:

  • uri (RDF::URI)

    the URI for the resource to be found

  • data (RDF::Repository)

    a repostiory instance in which to find the resource.

Returns:

  • (RDF::LDP::Resource)

    a resource instance matching the given URI; usually of a subclass from the interaction models.

Raises:



131
132
133
134
135
136
137
138
139
140
# File 'lib/rdf/ldp/resource.rb', line 131

def find(uri, data)
  graph = RDF::Graph.new(graph_name: 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)
end

.gen_idString

Note:

the current implementation uses SecureRandom#uuid.

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

Returns:

  • (String)

    a unique ID



115
116
117
# File 'lib/rdf/ldp/resource.rb', line 115

def gen_id
  SecureRandom.uuid
end

.interaction_model(link_header) ⇒ Class

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).

Parameters:

  • link_header (String)

    a string containing Link headers from an HTTP request (Rack env)

Returns:



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/rdf/ldp/resource.rb', line 155

def interaction_model(link_header)
  models =
    LinkHeader.parse(link_header)
              .links.select { |link| link['rel'].casecmp '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
      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

.metagraph_name(uri) ⇒ Object

Build a graph name URI for the uri passed in

Parameters:

  • uri (RDF::URI)


180
181
182
# File 'lib/rdf/ldp/resource.rb', line 180

def metagraph_name(uri)
  uri + '#meta'
end

.to_uriRDF::URI

Returns uri with lexical representation ‘www.w3.org/ns/ldp#Resource’.

Returns:

See Also:



105
106
107
# File 'lib/rdf/ldp/resource.rb', line 105

def to_uri
  RDF::Vocab::LDP.Resource
end

Instance Method Details

#allowed_methodsArray<Symbol>

Returns a list of HTTP methods allowed by this resource.

Returns:

  • (Array<Symbol>)

    a list of HTTP methods allowed by this resource.



359
360
361
362
363
# File 'lib/rdf/ldp/resource.rb', line 359

def allowed_methods
  [:GET, :POST, :PUT, :DELETE, :PATCH, :OPTIONS, :HEAD].select do |m|
    respond_to?(m.downcase, true)
  end
end

#container?Boolean

Returns whether this is an ldp:Container.

Returns:

  • (Boolean)

    whether this is an ldp:Container



373
374
375
# File 'lib/rdf/ldp/resource.rb', line 373

def container?
  false
end

#containersArray<RDF::LDP::Resource>

Returns the container for this resource.

Returns:



391
392
393
394
395
# File 'lib/rdf/ldp/resource.rb', line 391

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) {|tx| ... } ⇒ RDF::LDP::Resource

This method is abstract.

creates the resource

Returns self.

Parameters:

  • input (IO, File)

    input (usually from a Rack env’s ‘rack.input` key) used to determine the Resource’s initial state.

  • content_type (#to_s)

    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).

Yields:

  • gives a transaction (changeset) to collect changes to graph, metagraph and other resources’ (e.g. containers) graphs

Yield Parameters:

  • tx (RDF::Transaction)

Returns:

Raises:



225
226
227
228
229
230
231
232
233
234
235
# File 'lib/rdf/ldp/resource.rb', line 225

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 {|tx| ... } ⇒ RDF::LDP::Resource

TODO:

Use of owl:Nothing is probably problematic. Define an internal

Mark the resource as destroyed.

This adds a statment to the metagraph expressing that the resource has been deleted

namespace and class represeting deletion status as a stateful property.

Yields:

  • gives a transaction (changeset) to collect changes to graph, metagraph and other resources’ (e.g. containers) graphs

Yield Parameters:

  • tx (RDF::Transaction)

Returns:



276
277
278
279
280
281
282
283
284
285
286
# File 'lib/rdf/ldp/resource.rb', line 276

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?Boolean

Returns true if resource has been destroyed.

Returns:

  • (Boolean)

    true if resource has been destroyed



301
302
303
304
# File 'lib/rdf/ldp/resource.rb', line 301

def destroyed?
  times = @metagraph.query([subject_uri, INVALIDATED_AT_URI, nil])
  !times.empty?
end

#etagString

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 tools.ietf.org/html/rfc7232#section-3.1. See: github.com/ruby-rdf/rdf-ldp/issues/68

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

Returns:

  • (String)

    an HTTP Etag

See Also:



320
321
322
323
# File 'lib/rdf/ldp/resource.rb', line 320

def etag
  return nil unless exists?
  "W/\"#{last_modified.new_offset(0).iso8601(9)}\""
end

#exists?Boolean

Note:

destroyed resources continue to exist in the sense represeted by this method.

Gives the status of the resource’s existance.

Returns:

  • (Boolean)

    true if the resource exists within the repository



295
296
297
# File 'lib/rdf/ldp/resource.rb', line 295

def exists?
  @data.has_graph? metagraph.graph_name
end

#last_modifiedDateTime

TODO:

handle cases where there is more than one RDF::DC.modified. check for the most recent date

Returns the time this resource was last modified; ‘nil` if the resource doesn’t exist and has no modified date.

Returns:

  • (DateTime)

    the time this resource was last modified; ‘nil` if the resource doesn’t exist and has no modified date

Raises:



333
334
335
336
337
338
339
340
341
342
# File 'lib/rdf/ldp/resource.rb', line 333

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?Boolean

Returns whether this is an ldp:Resource.

Returns:

  • (Boolean)

    whether this is an ldp:Resource



367
368
369
# File 'lib/rdf/ldp/resource.rb', line 367

def ldp_resource?
  true
end

#match?(tag) ⇒ Boolean

Returns whether the given tag matches ‘#etag`.

Parameters:

  • tag (String)

    a tag to compare to ‘#etag`

Returns:

  • (Boolean)

    whether the given tag matches ‘#etag`



347
348
349
# File 'lib/rdf/ldp/resource.rb', line 347

def match?(tag)
  tag == etag
end

#non_rdf_source?Boolean

Returns whether this is an ldp:NonRDFSource.

Returns:

  • (Boolean)

    whether this is an ldp:NonRDFSource



379
380
381
# File 'lib/rdf/ldp/resource.rb', line 379

def non_rdf_source?
  false
end

#rdf_source?Boolean

Returns whether this is an ldp:RDFSource.

Returns:

  • (Boolean)

    whether this is an ldp:RDFSource



385
386
387
# File 'lib/rdf/ldp/resource.rb', line 385

def rdf_source?
  false
end

#request(method, status, headers, env) ⇒ Array<Fixnum, Hash<String, String>, #each] a new Rack response array.

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.

Parameters:

  • method (#to_sym)

    the HTTP request method of the response; this message will be downcased and sent to the object.

  • status (Fixnum)

    an HTTP response code; this status should be sent back to the caller or altered, as appropriate.

  • headers (Hash<String, String>)

    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.

  • env (Hash)

    the Rack env for the request

Returns:

  • (Array<Fixnum, Hash<String, String>, #each] a new Rack response array.)

    Array<Fixnum, Hash<String, String>, #each] a new Rack response array.

Raises:



432
433
434
435
436
437
438
439
# File 'lib/rdf/ldp/resource.rb', line 432

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_responseObject Also known as: each

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



403
404
405
# File 'lib/rdf/ldp/resource.rb', line 403

def to_response
  []
end

#to_uriRDF::URI

Returns the subject URI for this resource.

Returns:

  • (RDF::URI)

    the subject URI for this resource



353
354
355
# File 'lib/rdf/ldp/resource.rb', line 353

def to_uri
  subject_uri
end

#update(input, content_type) {|tx| ... } ⇒ RDF::LDP::Resource

This method is abstract.

update the resource

Returns self.

Parameters:

  • input (IO, File, #to_s)

    input (usually from a Rack env’s ‘rack.input` key) used to determine the Resource’s new state.

  • content_type (#to_s)

    a MIME content_type used to interpret the input.

Yields:

  • gives a transaction (changeset) to collect changes to graph, metagraph and other resources’ (e.g. containers) graphs

Yield Parameters:

  • tx (RDF::Transaction)

Returns:

Raises:

  • (RDF::LDP::RequestError)

    when update fails. May raise various subclasses for the appropriate response codes.



252
253
254
255
256
257
258
259
260
261
# File 'lib/rdf/ldp/resource.rb', line 252

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