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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/IMPLEMENTATION.md +280 -0
- data/README.md +72 -11
- data/VERSION +1 -1
- data/bin/lamprey +9 -0
- data/lib/rdf/ldp/container.rb +30 -0
- data/lib/rdf/ldp/direct_container.rb +61 -14
- data/lib/rdf/ldp/indirect_container.rb +81 -0
- data/lib/rdf/ldp/non_rdf_source.rb +237 -0
- data/lib/rdf/ldp/rdf_source.rb +84 -21
- data/lib/rdf/ldp/resource.rb +91 -5
- data/lib/rdf/ldp.rb +44 -1
- metadata +21 -3
@@ -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
|
data/lib/rdf/ldp/rdf_source.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
require 'digest/
|
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::
|
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
|
-
|
127
|
-
|
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
|
-
|
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]
|
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
|
-
|
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(
|
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
|
data/lib/rdf/ldp/resource.rb
CHANGED
@@ -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
|
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
|
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
|
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
|