rdf-ldp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,22 @@
1
1
  module RDF::LDP
2
+ ##
3
+ # An extension of `RDF::LDP::DirectContainer` implementing indirect
4
+ # containment. Adds the concept of an inserted content relation to the
5
+ # features of the direct container.
6
+ #
7
+ # Clients MUST provide exactly one `ldp:insertedContentRelation` statement in
8
+ # each Indirect Container. If no `#inserted_content_relation` is given by the
9
+ # client, we default to `ldp:MemberSubject`. If more than one is present,
10
+ #
11
+ # Attempts to POST resources without the appropriate content relation (or
12
+ # with more than one) to an Indirect Container will fail with `Not
13
+ # Acceptable`. LDP-NR's cannot be added since indirect membership is not well
14
+ # defined for them, per _LDP 5.5.1.2_.
15
+ #
16
+ # @see http://www.w3.org/TR/ldp/#h-ldpic-indirectmbr for an explanation if
17
+ # indirect membership and limitiations surrounding LDP-NRs.
18
+ # @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-indirect-container
19
+ # definition of LDP Indirect Container
2
20
  class IndirectContainer < DirectContainer
3
21
  def self.to_uri
4
22
  RDF::Vocab::LDP.IndirectContainer
@@ -9,5 +27,68 @@ module RDF::LDP
9
27
  def container_class
10
28
  CONTAINER_CLASSES[:indirect]
11
29
  end
30
+
31
+ ##
32
+ # Gives the inserted content relation for the indirect container. If none is
33
+ # present in the container state, we add `ldp:MemberSubject`, effectively
34
+ # treating this LDP-IC as an LDP-DC.
35
+ #
36
+ # @return [RDF::URI] the inserted content relation; either a predicate term
37
+ # or `ldp:MemberSubject`
38
+ #
39
+ # @raise [RDF::LDP::NotAcceptable] if multiple inserted content relations
40
+ # exist.
41
+ #
42
+ # @see http://www.w3.org/TR/ldp/#dfn-membership-triples
43
+ def inserted_content_relation
44
+ statements = inserted_content_statements
45
+ case statements.count
46
+ when 0
47
+ graph << RDF::Statement(subject_uri,
48
+ RDF::Vocab::LDP.indirectContentRelation,
49
+ RDF::Vocab::LDP.MemberSubject)
50
+ RDF::Vocab::LDP.MemberSubject
51
+ when 1
52
+ statements.first.object
53
+ else
54
+ raise NotAcceptable.new('An LDP-IC MUST have exactly ' \
55
+ 'one inserted content relation triple; found ' \
56
+ "#{statements.count}.")
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def inserted_content_statements
63
+ graph.statements.select do |st|
64
+ st.subject == subject_uri &&
65
+ st.predicate == RDF::Vocab::LDP.indirectContentRelation
66
+ end
67
+ end
68
+
69
+ def process_membership_resource(resource, &block)
70
+ resource = member_derived_uri(resource)
71
+ super(resource, &block)
72
+ end
73
+
74
+ def member_derived_uri(resource)
75
+ predicate = inserted_content_relation
76
+ return resource.to_uri if predicate == RDF::Vocab::LDP.MemberSubject
77
+
78
+ raise NotAcceptable.new("#{resource.to_uri} is an LDP-NR; cannot add " \
79
+ 'it to an IndirectContainer with a content ' \
80
+ 'relation.') if resource.non_rdf_source?
81
+
82
+ resource
83
+
84
+ statements = resource.graph.query([resource.subject_uri, predicate, :o])
85
+ case statements.count
86
+ when 1
87
+ statements.first.object
88
+ else
89
+ raise NotAcceptable.new("#{statements.count} inserted content" \
90
+ "#{predicate} found on #{resource.to_uri}")
91
+ end
92
+ end
12
93
  end
13
94
  end
@@ -1,5 +1,25 @@
1
1
  module RDF::LDP
2
+ ##
3
+ # A NonRDFSource describes a `Resource` whose response body is a format other
4
+ # than an RDF serialization. The persistent state of the resource, as
5
+ # represented by the body, is persisted to an IO stream provided by a
6
+ # `RDF::LDP::NonRDFSource::StorageAdapter` given by `#storage`.
7
+ #
8
+ # In addition to the properties stored by the `RDF::LDP::Resource#metagraph`,
9
+ # `NonRDFSource`s also store a content type (format).
10
+ #
11
+ # When a `NonRDFSource` is created, it also creates an `RDFSource` which
12
+ # describes it. This resource is created at the URI in `#description_uri`,
13
+ # the resource itself is returned by `#description`.
14
+ #
15
+ # @see RDF::LDP::Resource
16
+ # @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-non-rdf-source for
17
+ # a definition of NonRDFSource in LDP
2
18
  class NonRDFSource < Resource
19
+ # Use DC elements format
20
+ FORMAT_TERM = RDF::DC11.format
21
+ DESCRIBED_BY_TERM = RDF::URI('http://www.w3.org/2007/05/powder-s#describedby')
22
+
3
23
  ##
4
24
  # @return [RDF::URI] uri with lexical representation
5
25
  # 'http://www.w3.org/ns/ldp#NonRDFSource'
@@ -14,5 +34,222 @@ module RDF::LDP
14
34
  def non_rdf_source?
15
35
  true
16
36
  end
37
+
38
+ ##
39
+ # @param [IO, File] input input (usually from a Rack env's
40
+ # `rack.input` key) that will be read into the NonRDFSource
41
+ # @param [#to_s] c_type a MIME content_type used as a content type
42
+ # for the created NonRDFSource
43
+ #
44
+ # @raise [RDF::LDP::RequestError] when saving the NonRDFSource
45
+ #
46
+ # @return [RDF::LDP::NonRDFSource] self
47
+ #
48
+ # @see RDF::LDP::Resource#create
49
+ def create(input, c_type)
50
+ storage.io { |io| IO.copy_stream(input.binmode, io) }
51
+ super
52
+ self.content_type = c_type
53
+ RDFSource.new(description_uri, @data).create('', 'text/plain')
54
+ self
55
+ end
56
+
57
+ ##
58
+ # @see RDF::LDP::Resource#update
59
+ def update(input, c_type)
60
+ storage.io { |io| IO.copy_stream(input.binmode, io) }
61
+ self.content_type = c_type
62
+ self
63
+ end
64
+
65
+ ##
66
+ # Deletes the LDP-NR contents from the storage medium and marks the
67
+ # resource as destroyed.
68
+ #
69
+ # @see RDF::LDP::Resource#destroy
70
+ def destroy
71
+ storage.delete
72
+ super
73
+ end
74
+
75
+ def etag
76
+ "#{Digest::SHA1.base64digest(storage.io.read)}"
77
+ end
78
+
79
+ ##
80
+ # @raise [RDF::LDP::NotFound] if the describedby resource doesn't exist
81
+ #
82
+ # @return [RDF::LDP::RDFSource] resource describing this resource
83
+ def description
84
+ RDF::LDP::Resource.find(description_uri, @data)
85
+ end
86
+
87
+ ##
88
+ # @return [RDF::URI] uri for this resource's associated RDFSource
89
+ def description_uri
90
+ subject_uri / '.well-known' / 'desc'
91
+ end
92
+
93
+ ##
94
+ # @return [StorageAdapter] the storage adapter for this LDP-NR
95
+ def storage
96
+ @storage_adapter ||= StorageAdapter.new(self)
97
+ end
98
+
99
+ ##
100
+ # Sets the MIME type for the resource in `metagraph`.
101
+ #
102
+ # @param [String] a string representing the content type for this LDP-NR.
103
+ # This SHOULD be a regisered MIME type.
104
+ #
105
+ # @return [StorageAdapter] the content type
106
+ def content_type=(content_type)
107
+ metagraph.delete([subject_uri, FORMAT_TERM])
108
+ metagraph << RDF::Statement(subject_uri, RDF::DC11.format, content_type)
109
+ end
110
+
111
+ ##
112
+ # @return [StorageAdapter] this resource's content type
113
+ def content_type
114
+ format_triple = metagraph.first([subject_uri, FORMAT_TERM, :format])
115
+ format_triple.nil? ? nil : format_triple.object.object
116
+ end
117
+
118
+ ##
119
+ # @return [#each] the response body. This is normally the StorageAdapter's
120
+ # IO object in read and binary mode.
121
+ #
122
+ # @raise [RDF::LDP::RequestError] when the request fails
123
+ def to_response
124
+ (exists? && !destroyed?) ? storage.io : []
125
+ end
126
+
127
+ private
128
+
129
+ ##
130
+ # Process & generate response for PUT requsets.
131
+ def put(status, headers, env)
132
+ raise PreconditionFailed.new('Etag invalid') if
133
+ env.has_key?('HTTP_IF_MATCH') && !match?(env['HTTP_IF_MATCH'])
134
+
135
+ if exists?
136
+ update(env['rack.input'], env['CONTENT_TYPE'])
137
+ headers = update_headers(headers)
138
+ [200, headers, self]
139
+ else
140
+ create(env['rack.input'], env['CONTENT_TYPE'])
141
+ [201, update_headers(headers), self]
142
+ end
143
+ end
144
+
145
+ ##
146
+ # @see RDF::LDP::Resource#update_headers
147
+ def update_headers(headers)
148
+ headers['Content-Type'] = content_type
149
+ super
150
+ end
151
+
152
+ def link_headers
153
+ super << "<#{description_uri}>;rel=\"describedBy\""
154
+ end
155
+
156
+ ##
157
+ # StorageAdapters bundle the logic for mapping a `NonRDFSource` to a
158
+ # specific IO stream. Implementations must conform to a minimal interface:
159
+ #
160
+ # - `#initailize` must accept a `resource` parameter. The input should be
161
+ # a `NonRDFSource` (LDP-NR).
162
+ # - `#io` must yield and return a IO object in binary mode that represents
163
+ # the current state of the LDP-NR.
164
+ # - If a block is passed to `#io`, the implementation MUST allow return a
165
+ # writable IO object and that anything written to the stream while
166
+ # yielding is synced with the source in a thread-safe manner.
167
+ # - Clients not passing a block to `#io` SHOULD call `#close` on the
168
+ # object after reading it.
169
+ # - If the `#io` object responds to `#to_path` it MUST give the location
170
+ # of a file whose contents are identical the IO object's. This supports
171
+ # Rack's response body interface.
172
+ # - `#delete` remove the contents from the corresponding storage. This MAY
173
+ # be a no-op if is undesirable or impossible to delete the contents
174
+ # from the storage medium.
175
+ #
176
+ # @see http://www.rubydoc.info/github/rack/rack/master/file/SPEC#The_Body
177
+ # for details about `#to_path` in Rack response bodies.
178
+ #
179
+ # @example reading from a `StorageAdapter`
180
+ # storage = StorageAdapter.new(an_nr_source)
181
+ # storage.io.read # => [string contents of `an_nr_source`]
182
+ #
183
+ # @example writing to a `StorageAdapter`
184
+ # storage = StorageAdapter.new(an_nr_source)
185
+ # storage.io { |io| io.write('moomin')
186
+ #
187
+ # Beyond this interface, implementations are permitted to behave as desired.
188
+ # They may, for instance, reject undesirable content or alter the graph (or
189
+ # metagraph) of the resource. They should throw appropriate `RDF::LDP`
190
+ # errors when failing to allow the middleware to handle response codes and
191
+ # messages.
192
+ #
193
+ # The base storage adapter class provides a simple File storage
194
+ # implementation.
195
+ #
196
+ # @todo check thread saftey on write for base implementation
197
+ class StorageAdapter
198
+ STORAGE_PATH = '.storage'.freeze
199
+
200
+ ##
201
+ # Initializes the storage adapter.
202
+ #
203
+ # @param [NonRDFSource] resource
204
+ def initialize(resource)
205
+ @resource = resource
206
+ end
207
+
208
+ ##
209
+ # Gives an IO object which represents the current state of @resource.
210
+ # Opens the file for read-write (mode: r+), if it already exists;
211
+ # otherwise, creates the file and opens it for read-write (mode: w+).
212
+ #
213
+ # @yield [IO] yields a read-writable object conforming to the Ruby IO
214
+ # interface for storage. The IO object will be closed when the block
215
+ # ends.
216
+ #
217
+ # @return [IO] an object conforming to the Ruby IO interface
218
+ def io(&block)
219
+ FileUtils.mkdir_p(path_dir) unless Dir.exists?(path_dir)
220
+ FileUtils.touch(path) unless file_exists?
221
+
222
+ File.open(path, 'r+b', &block)
223
+ end
224
+
225
+ ##
226
+ # @return [Boolean] 1 if the file has been deleted, otherwise false
227
+ def delete
228
+ return false unless File.exists?(path)
229
+ File.delete(path)
230
+ end
231
+
232
+ private
233
+
234
+ ##
235
+ # @return [Boolean]
236
+ def file_exists?
237
+ File.exists?(path)
238
+ end
239
+
240
+ ##
241
+ # Build the path to the file on disk.
242
+ # @return [String]
243
+ def path
244
+ File.join(STORAGE_PATH, @resource.subject_uri.path)
245
+ end
246
+
247
+ ##
248
+ # Build the path to the file's directory on disk
249
+ # @return [String]
250
+ def path_dir
251
+ File.split(path).first
252
+ end
253
+ end
17
254
  end
18
255
  end
@@ -1,11 +1,11 @@
1
- require 'digest/md5'
1
+ require 'digest/sha1'
2
+ require 'ld/patch'
2
3
 
3
4
  module RDF::LDP
4
5
  ##
5
6
  # The base class for all directly usable LDP Resources that *are not*
6
7
  # `NonRDFSources`. RDFSources are implemented as a resource with:
7
8
  #
8
- # - a `#subject_uri` identifying the RDFSource (see: {RDF::LDP::Resource}).
9
9
  # - a `#graph` representing the "entire persistent state"
10
10
  # - a `#metagraph` containing internal properties of the RDFSource
11
11
  #
@@ -19,12 +19,13 @@ module RDF::LDP
19
19
  # LDP-server-managed triples. `#metagraph` contains statements internal
20
20
  # properties of the RDFSource which are necessary for the server's management
21
21
  # purposes, but MAY be absent from the representation of its state in `#graph`.
22
- # `#metagraph` is invisible to the client unless the implementation mirrors
23
- # its contents in `#graph`.
24
22
  #
25
23
  # @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-rdf-source definition
26
24
  # of ldp:RDFSource in the LDP specification
27
25
  class RDFSource < Resource
26
+
27
+ # @!attribute [rw] graph
28
+ # a graph representing the current persistent state of the resource.
28
29
  attr_accessor :graph
29
30
 
30
31
  class << self
@@ -37,7 +38,9 @@ module RDF::LDP
37
38
  RDF::Vocab::LDP.RDFSource
38
39
  end
39
40
  end
40
-
41
+
42
+ ##
43
+ # @see RDF::LDP::Resource#initialize
41
44
  def initialize(subject_uri, data = RDF::Repository.new)
42
45
  @graph = RDF::Graph.new(subject_uri, data: data)
43
46
  super
@@ -51,6 +54,11 @@ module RDF::LDP
51
54
  # `rack.input` key) used to determine the Resource's initial state.
52
55
  # @param [#to_s] content_type a MIME content_type used to read the graph.
53
56
  #
57
+ # @yield gives the new contents of `graph` to the caller's block before
58
+ # altering the state of the resource. This is useful when validation is
59
+ # required or triples are to be added by a subclass.
60
+ # @yieldparam [RDF::Enumerable] the contents parsed from input.
61
+ #
54
62
  # @raise [RDF::LDP::RequestError]
55
63
  # @raise [RDF::LDP::UnsupportedMediaType] if no reader can be found for the
56
64
  # graph
@@ -76,6 +84,15 @@ module RDF::LDP
76
84
  # @param [#to_s] content_type a MIME content_type used to interpret the
77
85
  # input.
78
86
  #
87
+ # @yield gives the new contents of `graph` to the caller's block before
88
+ # altering the state of the resource. This is useful when validation is
89
+ # required or triples are to be added by a subclass.
90
+ # @yieldparam [RDF::Enumerable] the triples parsed from input.
91
+ #
92
+ # @raise [RDF::LDP::RequestError]
93
+ # @raise [RDF::LDP::UnsupportedMediaType] if no reader can be found for the
94
+ # graph
95
+ #
79
96
  # @return [RDF::LDP::Resource] self
80
97
  def update(input, content_type, &block)
81
98
  return create(input, content_type) unless exists?
@@ -116,16 +133,15 @@ module RDF::LDP
116
133
  def etag
117
134
  subs = graph.subjects.map { |s| s.node? ? nil : s.to_s }
118
135
  .compact.sort.join()
119
- "\"#{Digest::MD5.base64digest(subs)}#{graph.statements.count}\""
136
+ "\"#{Digest::SHA1.base64digest(subs)}#{graph.statements.count}\""
120
137
  end
121
138
 
122
139
  ##
123
140
  # @param [String] tag a tag to compare to `#etag`
124
141
  # @return [Boolean] whether the given tag matches `#etag`
125
- def match?(tag)
126
- # return false unless tag.split('==').last == graph.statements.count.to_s
127
- tag == etag
128
- end
142
+ # def match?(tag)
143
+ # return false unless tag.split('==').last == graph.statements.count.to_s
144
+ # end
129
145
 
130
146
  ##
131
147
  # @return [Boolean] whether this is an ldp:RDFSource
@@ -133,12 +149,6 @@ module RDF::LDP
133
149
  true
134
150
  end
135
151
 
136
- ##
137
- # @return [RDF::URI] the subject URI for this resource
138
- def to_uri
139
- subject_uri
140
- end
141
-
142
152
  ##
143
153
  # Returns the graph representing this resource's state, without the graph
144
154
  # context.
@@ -148,11 +158,50 @@ module RDF::LDP
148
158
 
149
159
  private
150
160
 
161
+ ##
162
+ # Process & generate response for PUT requsets.
163
+ #
164
+ # @raise [RDF::LDP::UnsupportedMediaType] when a media type other than
165
+ # LDPatch is used
166
+ # @raise [RDF::LDP::BadRequest] when an invalid document is given
167
+ def patch(status, headers, env)
168
+ check_precondition!(env)
169
+ method = patch_types[env['CONTENT_TYPE']]
170
+
171
+ raise UnsupportedMediaType unless method
172
+
173
+ send(method, env['rack.input'], graph)
174
+ [200, update_headers(headers), self]
175
+ end
176
+
177
+ ##
178
+ # @return [Hash<String,Symbol>] a hash mapping supported PATCH content types
179
+ # to the method used to process the PATCH request
180
+ def patch_types
181
+ { 'text/ldpatch' => :ld_patch,
182
+ 'application/sparql-update' => :sparql_update }
183
+ end
184
+
185
+ def ld_patch(input, graph)
186
+ begin
187
+ LD::Patch.parse(input).execute(graph)
188
+ rescue LD::Patch::Error => e
189
+ raise BadRequest, e.message
190
+ end
191
+ end
192
+
193
+ def sparql_update(input, graph)
194
+ begin
195
+ SPARQL.execute(input, graph, update: true)
196
+ rescue SPARQL::MalformedQuery => e
197
+ raise BadRequest, e.message
198
+ end
199
+ end
200
+
151
201
  ##
152
202
  # Process & generate response for PUT requsets.
153
203
  def put(status, headers, env)
154
- raise PreconditionFailed.new('Etag invalid') if
155
- env.has_key?('HTTP_IF_MATCH') && !match?(env['HTTP_IF_MATCH'])
204
+ check_precondition!(env)
156
205
 
157
206
  if exists?
158
207
  update(env['rack.input'], env['CONTENT_TYPE'])
@@ -164,11 +213,21 @@ module RDF::LDP
164
213
  end
165
214
  end
166
215
 
216
+ ##
217
+ # @param [Hash<String, String>] env a rack env
218
+ # @raise [RDF::LDP::PreconditionFailed]
219
+ def check_precondition!(env)
220
+ raise PreconditionFailed.new('Etag invalid') if
221
+ env.has_key?('HTTP_IF_MATCH') && !match?(env['HTTP_IF_MATCH'])
222
+ end
223
+
224
+
167
225
  ##
168
226
  # Finds an {RDF::Reader} appropriate for the given content_type and attempts
169
227
  # to parse the graph string.
170
228
  #
171
- # @param [IO, File, String] graph an input stream to parse
229
+ # @param [IO, File, String] input a (Rack) input stream IO object or String
230
+ # to parse
172
231
  # @param [#to_s] content_type the content type for the reader
173
232
  #
174
233
  # @return [RDF::Enumerable] the statements in the resulting graph
@@ -177,11 +236,15 @@ module RDF::LDP
177
236
  #
178
237
  # @todo handle cases where no content type is given? Does RDF::Reader have
179
238
  # tools to help us here?
180
- def parse_graph(graph, content_type)
239
+ #
240
+ # @see http://www.rubydoc.info/github/rack/rack/file/SPEC#The_Input_Stream
241
+ # for documentation on input streams in the Rack SPEC
242
+ def parse_graph(input, content_type)
181
243
  reader = RDF::Reader.for(content_type: content_type.to_s)
182
244
  raise(RDF::LDP::UnsupportedMediaType, content_type) if reader.nil?
245
+ input = input.read if input.respond_to? :read
183
246
  begin
184
- RDF::Graph.new << reader.new(graph, base_uri: subject_uri)
247
+ RDF::Graph.new << reader.new(input, base_uri: subject_uri)
185
248
  rescue RDF::ReaderError => e
186
249
  raise RDF::LDP::BadRequest, e.message
187
250
  end
@@ -1,8 +1,53 @@
1
1
  require 'link_header'
2
2
 
3
3
  module RDF::LDP
4
+ ##
5
+ # The base class for all LDP Resources.
6
+ #
7
+ # The internal state of a Resource is specific to a given persistent datastore
8
+ # (an `RDF::Repository` passed to the initilazer) and is managed through an
9
+ # internal graph (`#metagraph`). A Resource has:
10
+ #
11
+ # - a `#subject_uri` identifying the Resource.
12
+ # - a `#metagraph` containing server-internal properties of the Resource.
13
+ #
14
+ # Resources also define a basic set of CRUD operations, identity and current
15
+ # state, and a `#to_response`/`#each` method used by Rack & `Rack::LDP` to
16
+ # generate an appropriate HTTP response body.
17
+ #
18
+ # `#metagraph' holds internal properites used by the server. It is distinct
19
+ # from, and may conflict with, other RDF and non-RDF information about the
20
+ # resource (e.g. representations suitable for a response body). Metagraph
21
+ # contains a canonical `rdf:type` statement, which specifies the resource's
22
+ # interaction model. If the resource is deleted, a flag in metagraph
23
+ # indicates this.
24
+ #
25
+ # The contents of `#metagraph` should not be confused with LDP
26
+ # server-managed-triples, Those triples are included in the state of the
27
+ # resource as represented by the response body. `#metagraph` is invisible to
28
+ # the client except where a subclass mirrors its contents in the body.
29
+ #
30
+ # @example creating a new Resource
31
+ # repository = RDF::Repository.new
32
+ # resource = RDF::LDP::Resource.new('http://example.org/moomin', repository)
33
+ # resource.exists? # => false
34
+ #
35
+ # resource.create('', 'text/plain')
36
+ #
37
+ # resource.exists? # => true
38
+ # resource.metagraph.dump :ttl
39
+ # # => "<http://example.org/moomin> a <http://www.w3.org/ns/ldp#Resource> ."
40
+ #
41
+ # @see http://www.w3.org/TR/ldp/ for the Linked Data platform specification
42
+ # @see http://www.w3.org/TR/ldp/#dfn-linked-data-platform-resource for a
43
+ # definition of 'Resource' in LDP
4
44
  class Resource
45
+ # @!attribute [r] subject_uri
46
+ # an rdf term
5
47
  attr_reader :subject_uri
48
+
49
+ # @!attribute [rw] metagraph
50
+ # a graph representing the server-internal state of the resource
6
51
  attr_accessor :metagraph
7
52
 
8
53
  class << self
@@ -82,6 +127,22 @@ module RDF::LDP
82
127
  end
83
128
  end
84
129
 
130
+ ##
131
+ # @param [RDF::URI, #to_s] subject_uri the uri that identifies the Resource
132
+ # @param [RDF::Repository] data the repository where the resource's RDF
133
+ # data (i.e. `metagraph`) is stored; defaults to an in-memory
134
+ # RDF::Repository specific to this Resource.
135
+ #
136
+ # @yield [RDF::Resource] Gives itself to the block
137
+ #
138
+ # @example
139
+ # RDF::Resource.new('http://example.org/moomin')
140
+ #
141
+ # @example with a block
142
+ # RDF::Resource.new('http://example.org/moomin') do |resource|
143
+ # resource.metagraph << RDF::Statement(...)
144
+ # end
145
+ #
85
146
  def initialize(subject_uri, data = RDF::Repository.new)
86
147
  @subject_uri = RDF::URI(subject_uri)
87
148
  @data = data
@@ -92,7 +153,7 @@ module RDF::LDP
92
153
  ##
93
154
  # @abstract creates the resource
94
155
  #
95
- # @param [IO, File, #to_s] input input (usually from a Rack env's
156
+ # @param [IO, File] input input (usually from a Rack env's
96
157
  # `rack.input` key) used to determine the Resource's initial state.
97
158
  # @param [#to_s] content_type a MIME content_type used to interpret the
98
159
  # input. This MAY be used as a content type for the created Resource
@@ -100,6 +161,7 @@ module RDF::LDP
100
161
  #
101
162
  # @raise [RDF::LDP::RequestError] when creation fails. May raise various
102
163
  # subclasses for the appropriate response codes.
164
+ # @raise [RDF::LDP::Conflict] when the resource exists
103
165
  #
104
166
  # @return [RDF::LDP::Resource] self
105
167
  def create(input, content_type)
@@ -152,6 +214,23 @@ module RDF::LDP
152
214
  !(@metagraph.query([subject_uri, RDF.type, RDF::OWL.Nothing]).empty?)
153
215
  end
154
216
 
217
+ def etag
218
+ nil
219
+ end
220
+
221
+ ##
222
+ # @param [String] tag a tag to compare to `#etag`
223
+ # @return [Boolean] whether the given tag matches `#etag`
224
+ def match?(tag)
225
+ tag == etag
226
+ end
227
+
228
+ ##
229
+ # @return [RDF::URI] the subject URI for this resource
230
+ def to_uri
231
+ subject_uri
232
+ end
233
+
155
234
  ##
156
235
  # @return [Array<Symbol>] a list of HTTP methods allowed by this resource.
157
236
  def allowed_methods
@@ -223,7 +302,7 @@ module RDF::LDP
223
302
  # @param [Hash<String, String>] headers a hash mapping HTTP headers
224
303
  # built for the response to their contents; these headers should be sent
225
304
  # back to the caller or altered, as appropriate.
226
- # @param [] env the Rack env for the request
305
+ # @param [Hash] env the Rack env for the request
227
306
  #
228
307
  # @return [Array<Fixnum, Hash<String, String>, #each] a new Rack response
229
308
  # array.
@@ -231,7 +310,7 @@ module RDF::LDP
231
310
  raise Gone if destroyed?
232
311
  begin
233
312
  send(method.to_sym.downcase, status, headers, env)
234
- rescue NotImplementedError => e
313
+ rescue NotImplementedError, NoMethodError => e
235
314
  raise MethodNotAllowed, method
236
315
  end
237
316
  end
@@ -279,9 +358,10 @@ module RDF::LDP
279
358
  ([headers['Link']] + link_headers).compact.join(",")
280
359
 
281
360
  headers['Allow'] = allowed_methods.join(', ')
282
- headers['Accept-Post'] = accept_post if respond_to?(:post, true)
361
+ headers['Accept-Post'] = accept_post if respond_to?(:post, true)
362
+ headers['Accept-Patch'] = accept_patch if respond_to?(:patch, true)
283
363
 
284
- headers['ETag'] ||= etag if respond_to?(:etag)
364
+ headers['ETag'] ||= etag if etag
285
365
  headers
286
366
  end
287
367
 
@@ -291,6 +371,12 @@ module RDF::LDP
291
371
  RDF::Reader.map { |klass| klass.format.content_type }.flatten.join(', ')
292
372
  end
293
373
 
374
+ ##
375
+ # @return [String] the Accept-Patch headers
376
+ def accept_patch
377
+ respond_to?(:patch_types, true) ? patch_types.keys.join(',') : ''
378
+ end
379
+
294
380
  ##
295
381
  # @return [Array<String>] an array of link headers to add to the
296
382
  # existing ones