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"^^xsd:dateTime ."

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

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



196
197
198
199
200
201
# 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

Instance Attribute Details

#metagraphObject

a graph representing the server-internal state of the resource



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

def metagraph
  @metagraph
end

#subject_uriObject (readonly)

Returns the value of attribute subject_uri.



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

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:



132
133
134
135
136
137
138
139
140
141
142
143
# 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_idString

Note:

the current implementation uses SecureRandom#uuid.

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

Returns:

  • (String)

    a unique ID



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

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:

Raises:



158
159
160
161
162
163
164
165
166
167
168
169
# 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) ⇒ Object

Build a graph name URI for the uri passed in

Parameters:

  • uri (RDF::URI)


175
176
177
# File 'lib/rdf/ldp/resource.rb', line 175

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

.to_uriRDF::URI

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

Returns:

See Also:



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

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.



354
355
356
357
358
# 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?Boolean

Returns whether this is an ldp:Container.

Returns:

  • (Boolean)

    whether this is an ldp:Container



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

def container?
  false
end

#containersArray<RDF::LDP::Resource>

Returns the container for this resource.

Returns:



386
387
388
389
390
# 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) {|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:



220
221
222
223
224
225
226
227
228
229
230
# 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 {|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:



271
272
273
274
275
276
277
278
279
280
281
# 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?Boolean

Returns true if resource has been destroyed.

Returns:

  • (Boolean)

    true if resource has been destroyed



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

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:



315
316
317
318
# File 'lib/rdf/ldp/resource.rb', line 315

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



290
291
292
# File 'lib/rdf/ldp/resource.rb', line 290

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:



328
329
330
331
332
333
334
335
336
337
# 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?Boolean

Returns whether this is an ldp:Resource.

Returns:

  • (Boolean)

    whether this is an ldp:Resource



362
363
364
# File 'lib/rdf/ldp/resource.rb', line 362

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`



342
343
344
# File 'lib/rdf/ldp/resource.rb', line 342

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



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

def non_rdf_source?
  false
end

#rdf_source?Boolean

Returns whether this is an ldp:RDFSource.

Returns:

  • (Boolean)

    whether this is an ldp:RDFSource



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

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:



427
428
429
430
431
432
433
434
# 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_responseObject Also known as: each

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



398
399
400
# File 'lib/rdf/ldp/resource.rb', line 398

def to_response
  []
end

#to_uriRDF::URI

Returns the subject URI for this resource.

Returns:

  • (RDF::URI)

    the subject URI for this resource



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

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.



247
248
249
250
251
252
253
254
255
256
# 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