wf4ever-rosrs-client 0.1.1
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.
- data/.rvmrc +1 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +20 -0
- data/README.md +180 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/lib/wf4ever/rosrs/annotation.rb +76 -0
- data/lib/wf4ever/rosrs/exceptions.rb +19 -0
- data/lib/wf4ever/rosrs/folder.rb +127 -0
- data/lib/wf4ever/rosrs/folder_entry.rb +44 -0
- data/lib/wf4ever/rosrs/helper.rb +12 -0
- data/lib/wf4ever/rosrs/namespaces.rb +42 -0
- data/lib/wf4ever/rosrs/rdf_graph.rb +72 -0
- data/lib/wf4ever/rosrs/research_object.rb +233 -0
- data/lib/wf4ever/rosrs/resource.rb +59 -0
- data/lib/wf4ever/rosrs/session.rb +672 -0
- data/lib/wf4ever/rosrs_client.rb +20 -0
- data/test/helper.rb +17 -0
- data/test/test_abstract_interaction.rb +200 -0
- data/test/test_rosrs_session.rb +393 -0
- metadata +166 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
# ROSRS session class
|
|
2
|
+
module ROSRS
|
|
3
|
+
class Session
|
|
4
|
+
|
|
5
|
+
attr_reader :uri
|
|
6
|
+
|
|
7
|
+
ANNOTATION_CONTENT_TYPES =
|
|
8
|
+
{ "application/rdf+xml" => :xml,
|
|
9
|
+
"text/turtle" => :turtle,
|
|
10
|
+
#"text/n3" => :n3,
|
|
11
|
+
"text/nt" => :ntriples,
|
|
12
|
+
#"application/json" => :jsonld,
|
|
13
|
+
#"application/xhtml" => :rdfa,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
PARSEABLE_CONTENT_TYPES = ['application/vnd.wf4ever.folderentry',
|
|
17
|
+
'application/vnd.wf4ever.folder',
|
|
18
|
+
'application/rdf+xml']
|
|
19
|
+
|
|
20
|
+
# -------------
|
|
21
|
+
# General setup
|
|
22
|
+
# -------------
|
|
23
|
+
|
|
24
|
+
def initialize(uri, accesskey=nil)
|
|
25
|
+
@uri = URI(uri.to_s) # Force string or URI to be a URI - tried coerce, didn't work
|
|
26
|
+
@key = accesskey
|
|
27
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def close
|
|
31
|
+
if @http and @http.started?
|
|
32
|
+
@http.finish
|
|
33
|
+
@http = nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error(code, msg, value=nil)
|
|
38
|
+
# Raise exception with supplied message and optional value
|
|
39
|
+
if value
|
|
40
|
+
msg += " (#{value})"
|
|
41
|
+
end
|
|
42
|
+
msg = "Exception on #@uri #{msg}"
|
|
43
|
+
case code
|
|
44
|
+
when 401
|
|
45
|
+
raise ROSRS::UnauthorizedException.new(msg)
|
|
46
|
+
when 403
|
|
47
|
+
raise ROSRS::ForbiddenException.new(msg)
|
|
48
|
+
when 404
|
|
49
|
+
raise ROSRS::NotFoundException.new(msg)
|
|
50
|
+
when 409
|
|
51
|
+
raise ROSRS::ConflictException.new(msg)
|
|
52
|
+
else
|
|
53
|
+
raise ROSRS::Exception.new(msg)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# -------
|
|
58
|
+
# Helpers
|
|
59
|
+
# -------
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# Parse links from headers; returns a hash indexed by link relation
|
|
65
|
+
# Headerlist is a hash indexed by header field name (see HTTP:Response)
|
|
66
|
+
def parse_links(headers)
|
|
67
|
+
links = {}
|
|
68
|
+
link_header = headers["link"] || headers["Link"]
|
|
69
|
+
link_header.split(",").each do |link|
|
|
70
|
+
matches = link.strip.match(/<([^>]*)>\s*;.*rel\s*=\s*"?([^;"]*)"?/)
|
|
71
|
+
if matches
|
|
72
|
+
links[matches[2]] ||= []
|
|
73
|
+
links[matches[2]] << matches[1]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
links
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Extract path (incl query) for HTTP request
|
|
81
|
+
# Should accept URI, RDF::URI or string values
|
|
82
|
+
# Must be same host and port as session URI
|
|
83
|
+
# Relative values are based on session URI
|
|
84
|
+
def get_request_path(uripath)
|
|
85
|
+
uripath = URI(uripath.to_s)
|
|
86
|
+
if uripath.scheme && (uripath.scheme != @uri.scheme)
|
|
87
|
+
error(nil, "Request URI scheme does not match session: #{uripath}")
|
|
88
|
+
end
|
|
89
|
+
if (uripath.host && uripath.host != @uri.host) ||
|
|
90
|
+
(uripath.port && uripath.port != @uri.port)
|
|
91
|
+
error(nil, "Request URI host or port does not match session: #{uripath}")
|
|
92
|
+
end
|
|
93
|
+
requri = URI.join(@uri.to_s, uripath.path).path
|
|
94
|
+
if uripath.query
|
|
95
|
+
requri += "?"+uripath.query
|
|
96
|
+
end
|
|
97
|
+
requri
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def get_request_headers(options = {})
|
|
101
|
+
if options[:headers]
|
|
102
|
+
# Convert symbol keys to strings
|
|
103
|
+
reqheaders = options[:headers].inject({}) do |headers, (header, value)|
|
|
104
|
+
headers[header.to_s] = value
|
|
105
|
+
headers
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
reqheaders = {}
|
|
109
|
+
end
|
|
110
|
+
if @key
|
|
111
|
+
reqheaders["authorization"] = "Bearer "+@key
|
|
112
|
+
end
|
|
113
|
+
if options[:ctype]
|
|
114
|
+
reqheaders["content-type"] = options[:ctype]
|
|
115
|
+
end
|
|
116
|
+
if options[:accept]
|
|
117
|
+
reqheaders['accept'] = options[:accept]
|
|
118
|
+
end
|
|
119
|
+
if options[:link]
|
|
120
|
+
reqheaders['Link'] = options[:link]
|
|
121
|
+
end
|
|
122
|
+
reqheaders
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
public
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Perform HTTP request
|
|
129
|
+
#
|
|
130
|
+
# +method+:: HTTP method name
|
|
131
|
+
# +uripath+:: is reference or URI of resource (see get_request_path)
|
|
132
|
+
# options:
|
|
133
|
+
# [:body] body to accompany request
|
|
134
|
+
# [:ctype] content type of supplied body
|
|
135
|
+
# [:accept] accept content types for response
|
|
136
|
+
# [:headers] additional headers for request
|
|
137
|
+
# Return [code, reason(text), response headers, response body]
|
|
138
|
+
#
|
|
139
|
+
def do_request(method, uripath, options = {})
|
|
140
|
+
|
|
141
|
+
req = nil
|
|
142
|
+
|
|
143
|
+
case method
|
|
144
|
+
when 'GET'
|
|
145
|
+
req = Net::HTTP::Get.new(get_request_path(uripath))
|
|
146
|
+
when 'PUT'
|
|
147
|
+
req = Net::HTTP::Put.new(get_request_path(uripath))
|
|
148
|
+
when 'POST'
|
|
149
|
+
req = Net::HTTP::Post.new(get_request_path(uripath))
|
|
150
|
+
when 'DELETE'
|
|
151
|
+
req = Net::HTTP::Delete.new(get_request_path(uripath))
|
|
152
|
+
else
|
|
153
|
+
error(nil, "Unrecognized HTTP method #{method}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if options[:body]
|
|
157
|
+
req.body = options[:body]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
get_request_headers(options).each { |h,v| req.add_field(h, v) }
|
|
161
|
+
resp = @http.request(req)
|
|
162
|
+
[Integer(resp.code), resp.message, resp, resp.body]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
##
|
|
166
|
+
# Perform HTTP request, following 302, 303 307 redirects
|
|
167
|
+
# Return [code, reason(text), response headers, final uri, response body]
|
|
168
|
+
def do_request_follow_redirect(method, uripath, options = {})
|
|
169
|
+
code, reason, headers, data = do_request(method, uripath, options)
|
|
170
|
+
if [302,303,307].include?(code)
|
|
171
|
+
uripath = headers["location"]
|
|
172
|
+
code, reason, headers, data = do_request(method, uripath, options)
|
|
173
|
+
end
|
|
174
|
+
if [302,307].include?(code)
|
|
175
|
+
# Allow second temporary redirect
|
|
176
|
+
uripath = headers["location"]
|
|
177
|
+
code, reason, headers, data = do_request(method, uripath, options)
|
|
178
|
+
end
|
|
179
|
+
[code, reason, headers, uripath, data]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
##
|
|
183
|
+
# Perform HTTP request expecting an RDF/XML response
|
|
184
|
+
# Return [code, reason(text), response headers, manifest graph]
|
|
185
|
+
# Returns the manifest as a graph if the request is successful
|
|
186
|
+
# otherwise returns the raw response data.
|
|
187
|
+
def do_request_rdf(method, uripath, options = {})
|
|
188
|
+
options[:accept] ||= "application/rdf+xml"
|
|
189
|
+
code, reason, headers, uripath, data = do_request_follow_redirect(method, uripath, options)
|
|
190
|
+
if code >= 200 and code < 300
|
|
191
|
+
begin
|
|
192
|
+
data = ROSRS::RDFGraph.new(:data => data, :format => :xml)
|
|
193
|
+
rescue Exception => e
|
|
194
|
+
code = 902
|
|
195
|
+
reason = "RDF parse failure (#{e.message})"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
[code, reason, headers, uripath, data]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# ---------------
|
|
202
|
+
# RO manipulation
|
|
203
|
+
# ---------------
|
|
204
|
+
|
|
205
|
+
##
|
|
206
|
+
# Returns [copde, reason, uri, manifest]
|
|
207
|
+
def create_research_object(name)
|
|
208
|
+
code, reason, headers, uripath, data = do_request_rdf("POST", "",
|
|
209
|
+
:headers => {'slug' => name})
|
|
210
|
+
if code == 201
|
|
211
|
+
[code, reason, headers["location"], data]
|
|
212
|
+
else
|
|
213
|
+
error(code, "Error creating RO: #{code} #{reason}")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def delete_research_object(ro_uri)
|
|
218
|
+
# code, reason = delete_research_object(ro_uri)
|
|
219
|
+
code, reason = do_request("DELETE", ro_uri,
|
|
220
|
+
:accept => "application/rdf+xml")
|
|
221
|
+
if [204, 404].include?(code)
|
|
222
|
+
[code, reason]
|
|
223
|
+
else
|
|
224
|
+
error(code, "Error deleting RO #{ro_uri}: #{code} #{reason}")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# ---------------------
|
|
229
|
+
# Resource manipulation
|
|
230
|
+
# ---------------------
|
|
231
|
+
|
|
232
|
+
##
|
|
233
|
+
# Aggregate internal resource
|
|
234
|
+
#
|
|
235
|
+
# options:
|
|
236
|
+
# [:body] body to accompany request
|
|
237
|
+
# [:ctype] content type of supplied body
|
|
238
|
+
# [:accept] accept content types for response
|
|
239
|
+
# [:headers] additional headers for request
|
|
240
|
+
#
|
|
241
|
+
# Returns: [code, reason, proxyuri, resource_uri], where code is 200 or 201
|
|
242
|
+
|
|
243
|
+
def aggregate_internal_resource(ro_uri, respath=nil, options={})
|
|
244
|
+
if respath
|
|
245
|
+
options[:headers] ||= {}
|
|
246
|
+
options[:headers]['slug'] = respath
|
|
247
|
+
end
|
|
248
|
+
# POST resource content to indicated URI
|
|
249
|
+
code, reason, headers = do_request("POST", ro_uri, options)
|
|
250
|
+
unless [200,201].include?(code)
|
|
251
|
+
error(code, "Error creating aggregated resource content",
|
|
252
|
+
"#{code}, #{reason}, #{respath}")
|
|
253
|
+
end
|
|
254
|
+
proxyuri = headers["location"]
|
|
255
|
+
resource_uri = parse_links(headers)[RDF::ORE.proxyFor.to_s].first
|
|
256
|
+
[code, reason, proxyuri, resource_uri]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
##
|
|
261
|
+
# Aggregate external resource
|
|
262
|
+
#
|
|
263
|
+
# Returns: [code, reason, proxyuri, resource_uri], where code is 200 or 201
|
|
264
|
+
def aggregate_external_resource(ro_uri, resource_uri=nil)
|
|
265
|
+
proxydata = %(
|
|
266
|
+
<rdf:RDF
|
|
267
|
+
xmlns:ore="http://www.openarchives.org/ore/terms/"
|
|
268
|
+
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" >
|
|
269
|
+
<ore:Proxy>
|
|
270
|
+
<ore:proxyFor rdf:resource="#{resource_uri}"/>
|
|
271
|
+
</ore:Proxy>
|
|
272
|
+
</rdf:RDF>
|
|
273
|
+
)
|
|
274
|
+
code, reason, headers = do_request("POST", ro_uri,
|
|
275
|
+
:ctype => "application/vnd.wf4ever.proxy",
|
|
276
|
+
:body => proxydata)
|
|
277
|
+
if code != 201
|
|
278
|
+
error(code, "Error creating aggregation proxy",
|
|
279
|
+
"#{code} #{reason} #{resource_uri}")
|
|
280
|
+
else
|
|
281
|
+
proxyuri = headers["location"]
|
|
282
|
+
resuri = parse_links(headers)[RDF::ORE.proxyFor.to_s].first
|
|
283
|
+
[code, reason, proxyuri, resuri]
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# -----------------------
|
|
288
|
+
# Resource access
|
|
289
|
+
# -----------------------
|
|
290
|
+
|
|
291
|
+
##
|
|
292
|
+
# Retrieve resource from RO
|
|
293
|
+
#
|
|
294
|
+
# resuriref is relative reference or URI of resource
|
|
295
|
+
# ro_uri is URI of RO, used as base for relative reference
|
|
296
|
+
# options:
|
|
297
|
+
# [:accept] content type
|
|
298
|
+
# [:headers] additional headers for request
|
|
299
|
+
# Returns:
|
|
300
|
+
# [code, reason, headers, data], where code is 200 or 404
|
|
301
|
+
def get_resource(resuriref, ro_uri=nil, options={})
|
|
302
|
+
if ro_uri
|
|
303
|
+
resuriref = URI.join(ro_uri.to_s, resuriref.to_s)
|
|
304
|
+
end
|
|
305
|
+
code, reason, headers, uri, data = do_request_follow_redirect("GET", resuriref, options)
|
|
306
|
+
if parseable?(headers["content-type"])
|
|
307
|
+
data = ROSRS::RDFGraph.new(:data => data, :format => :xml)
|
|
308
|
+
end
|
|
309
|
+
unless [200,404].include?(code)
|
|
310
|
+
error(code, "Error retrieving RO resource: #{code}, #{reason}, #{resuriref}")
|
|
311
|
+
end
|
|
312
|
+
[code, reason, headers, uri, data]
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
##
|
|
316
|
+
# Retrieve RDF resource from RO
|
|
317
|
+
#
|
|
318
|
+
# resource_uri is relative reference or URI of resource
|
|
319
|
+
# ro_uri is URI of RO, used as base for relative reference
|
|
320
|
+
# options:
|
|
321
|
+
# [:headers] additional headers for request
|
|
322
|
+
#
|
|
323
|
+
# Returns:
|
|
324
|
+
# [code, reason, headers, uri, data], where code is 200 or 404
|
|
325
|
+
#
|
|
326
|
+
# If code isreturned as 200, data is returned as an RDFGraph value
|
|
327
|
+
def get_resource_rdf(resource_uri, ro_uri=nil, options={})
|
|
328
|
+
if ro_uri
|
|
329
|
+
resource_uri = URI.join(ro_uri.to_s, resource_uri.to_s)
|
|
330
|
+
end
|
|
331
|
+
code, reason, headers, uri, data = do_request_rdf("GET", resource_uri, options)
|
|
332
|
+
unless [200,404].include?(code)
|
|
333
|
+
error(code, "Error retrieving RO resource: #{code}, #{reason}, #{resource_uri}")
|
|
334
|
+
end
|
|
335
|
+
[code, reason, headers, uri, data]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
##
|
|
339
|
+
# Retrieve an RO manifest
|
|
340
|
+
#
|
|
341
|
+
# Returns [manifesturi, manifest]
|
|
342
|
+
def get_manifest(ro_uri)
|
|
343
|
+
code, reason, headers, uri, data = do_request_rdf("GET", ro_uri)
|
|
344
|
+
if code != 200
|
|
345
|
+
error(code, "Error retrieving RO manifest: #{code} #{reason}")
|
|
346
|
+
end
|
|
347
|
+
[uri, data]
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# -----------------------
|
|
351
|
+
# Annotation manipulation
|
|
352
|
+
# -----------------------
|
|
353
|
+
|
|
354
|
+
##
|
|
355
|
+
# Create an annotation body from a supplied annnotation graph.
|
|
356
|
+
#
|
|
357
|
+
# Returns: [code, reason, body_uri]
|
|
358
|
+
def create_annotation_body(ro_uri, annotation_graph)
|
|
359
|
+
code, reason, bodyproxyuri, body_uri = aggregate_internal_resource(ro_uri, nil,
|
|
360
|
+
:ctype => "application/rdf+xml",
|
|
361
|
+
:body => annotation_graph.serialize(format=:xml))
|
|
362
|
+
if code != 201
|
|
363
|
+
error(code, "Error creating annotation body resource",
|
|
364
|
+
"#{code}, #{reason}, #{ro_uri}")
|
|
365
|
+
end
|
|
366
|
+
[code, reason, body_uri]
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
##
|
|
370
|
+
# Create entity body for annotation stub
|
|
371
|
+
def create_annotation_stub_rdf(ro_uri, resource_uri, body_uri)
|
|
372
|
+
v = { :xmlbase => ro_uri.to_s,
|
|
373
|
+
:resource_uri => resource_uri.to_s,
|
|
374
|
+
:body_uri => body_uri.to_s
|
|
375
|
+
}
|
|
376
|
+
annotation_stub = %Q(<?xml version="1.0" encoding="UTF-8"?>
|
|
377
|
+
<rdf:RDF
|
|
378
|
+
xmlns:ro="http://purl.org/wf4ever/ro#"
|
|
379
|
+
xmlns:ao="http://purl.org/ao/"
|
|
380
|
+
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
381
|
+
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
|
|
382
|
+
xml:base="#{v[:xmlbase]}"
|
|
383
|
+
>
|
|
384
|
+
<ro:AggregatedAnnotation>
|
|
385
|
+
<ao:annotatesResource rdf:resource="#{v[:resource_uri]}" />
|
|
386
|
+
<ao:body rdf:resource="#{v[:body_uri]}" />
|
|
387
|
+
</ro:AggregatedAnnotation>
|
|
388
|
+
</rdf:RDF>
|
|
389
|
+
)
|
|
390
|
+
annotation_stub
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
##
|
|
394
|
+
# Create an annotation stub for supplied resource using indicated body
|
|
395
|
+
#
|
|
396
|
+
# Returns: [code, reason, stuburi]
|
|
397
|
+
def create_annotation_stub(ro_uri, resource_uri, body_uri)
|
|
398
|
+
annotation = create_annotation_stub_rdf(ro_uri, resource_uri, body_uri)
|
|
399
|
+
code, reason, headers, data = do_request("POST", ro_uri,
|
|
400
|
+
:ctype => "application/vnd.wf4ever.annotation",
|
|
401
|
+
:body => annotation)
|
|
402
|
+
if code != 201
|
|
403
|
+
error(code, "Error creating annotation #{code}, #{reason}, #{resource_uri}")
|
|
404
|
+
end
|
|
405
|
+
[code, reason, headers["location"]]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
##
|
|
409
|
+
# Create internal annotation
|
|
410
|
+
#
|
|
411
|
+
# Returns: [code, reason, annotation_uri, body_uri]
|
|
412
|
+
def create_internal_annotation(ro_uri, resource_uri, annotation_graph)
|
|
413
|
+
code, reason, headers, data = do_request("POST", ro_uri,
|
|
414
|
+
:ctype => "application/rdf+xml",
|
|
415
|
+
:body => annotation_graph.serialize(format=:xml),
|
|
416
|
+
:link => "<#{resource_uri}>; rel=\"#{RDF::AO.annotatesResource}\"")
|
|
417
|
+
if code != 201
|
|
418
|
+
error(code, "Error creating annotation #{code}, #{reason}, #{resource_uri}")
|
|
419
|
+
end
|
|
420
|
+
[code, reason, headers["location"], parse_links(headers)[RDF::AO.body.to_s].first]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
##
|
|
424
|
+
# Create a resource annotation using an existing (possibly external) annotation body
|
|
425
|
+
#
|
|
426
|
+
# Returns: (code, reason, annotation_uri)
|
|
427
|
+
def create_external_annotation(ro_uri, resource_uri, body_uri)
|
|
428
|
+
create_annotation_stub(ro_uri, resource_uri, body_uri)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
##
|
|
432
|
+
# Update an indicated annotation for supplied resource using indicated body
|
|
433
|
+
#
|
|
434
|
+
# Returns: [code, reason]
|
|
435
|
+
def update_annotation_stub(ro_uri, stuburi, resource_uri, body_uri)
|
|
436
|
+
annotation = create_annotation_stub_rdf(ro_uri, resource_uri, body_uri)
|
|
437
|
+
code, reason, headers, data = do_request("PUT", stuburi,
|
|
438
|
+
:ctype => "application/vnd.wf4ever.annotation",
|
|
439
|
+
:body => annotation)
|
|
440
|
+
if code != 200
|
|
441
|
+
error(code, "Error updating annotation #{code}, #{reason}, #{resource_uri}")
|
|
442
|
+
end
|
|
443
|
+
[code, reason]
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
##
|
|
447
|
+
# Update an annotation with a new internal annotation body
|
|
448
|
+
#
|
|
449
|
+
# returns: [code, reason, body_uri]
|
|
450
|
+
def update_internal_annotation(ro_uri, stuburi, resource_uri, annotation_graph)
|
|
451
|
+
code, reason, body_uri = create_annotation_body(ro_uri, annotation_graph)
|
|
452
|
+
if code != 201
|
|
453
|
+
error(code, "Error creating annotation #{code}, #{reason}, #{resource_uri}")
|
|
454
|
+
end
|
|
455
|
+
code, reason = update_annotation_stub(ro_uri, stuburi, resource_uri, body_uri)
|
|
456
|
+
[code, reason, body_uri]
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
##
|
|
460
|
+
# Update an annotation with an existing (possibly external) annotation body
|
|
461
|
+
#
|
|
462
|
+
# returns: (code, reason)
|
|
463
|
+
def update_external_annotation(ro_uri, annotation_uri, resource_uri, body_uri)
|
|
464
|
+
update_annotation_stub(ro_uri, annotation_uri, resource_uri, body_uri)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
##
|
|
468
|
+
# Enumerate annnotation URIs associated with a resource
|
|
469
|
+
# (or all annotations for an RO)
|
|
470
|
+
#
|
|
471
|
+
# Returns an array of annotation URIs
|
|
472
|
+
def get_annotation_stub_uris(ro_uri, resource_uri=nil)
|
|
473
|
+
manifesturi, manifest = get_manifest(ro_uri)
|
|
474
|
+
stuburis = []
|
|
475
|
+
manifest.query(:object => RDF::URI(resource_uri)) do |stmt|
|
|
476
|
+
if [RDF::AO.annotatesResource,RDF::RO.annotatesAggregatedResource].include?(stmt.predicate)
|
|
477
|
+
stuburis << stmt.subject
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
stuburis
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
##
|
|
484
|
+
# Enumerate annnotation body URIs associated with a resource
|
|
485
|
+
# (or all annotations for an RO)
|
|
486
|
+
#
|
|
487
|
+
# Returns an array of annotation body URIs
|
|
488
|
+
def get_annotation_body_uris(ro_uri, resource_uri=nil)
|
|
489
|
+
manifesturi, manifest = get_manifest(ro_uri)
|
|
490
|
+
body_uris = []
|
|
491
|
+
|
|
492
|
+
query1 = RDF::Query.new do
|
|
493
|
+
pattern [:annotation_uri, RDF::AO.annotatesResource, RDF::URI(resource_uri)]
|
|
494
|
+
pattern [:annotation_uri, RDF::AO.body, :body_uri]
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
query2 = RDF::Query.new do
|
|
498
|
+
pattern [:annotation_uri, RDF::RO.annotatesAggregatedResource, RDF::URI(resource_uri)]
|
|
499
|
+
pattern [:annotation_uri, RDF::AO.body, :body_uri]
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
manifest.query(query1) do |result|
|
|
503
|
+
body_uris << result.body_uri.to_s
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
manifest.query(query2) do |result|
|
|
507
|
+
body_uris << result.body_uri.to_s
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
body_uris.uniq
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
##
|
|
514
|
+
# Build RDF graph of all annnotations associated with a resource
|
|
515
|
+
# (or all annotations for an RO)
|
|
516
|
+
#
|
|
517
|
+
# Returns graph of merged annotations
|
|
518
|
+
def get_annotation_graph(ro_uri, resource_uri=nil)
|
|
519
|
+
annotation_graph = ROSRS::RDFGraph.new
|
|
520
|
+
get_annotation_body_uris(ro_uri, resource_uri).each do |auri|
|
|
521
|
+
code, reason, headers, buri, bodytext = do_request_follow_redirect("GET", auri, {})
|
|
522
|
+
if code == 200
|
|
523
|
+
content_type = headers['content-type'].split(';', 2)[0].strip.downcase
|
|
524
|
+
if ANNOTATION_CONTENT_TYPES.include?(content_type)
|
|
525
|
+
bodyformat = ANNOTATION_CONTENT_TYPES[content_type]
|
|
526
|
+
annotation_graph.load_data(bodytext, bodyformat)
|
|
527
|
+
else
|
|
528
|
+
warn("Warning: #{buri} has unrecognized content-type: #{content_type}")
|
|
529
|
+
end
|
|
530
|
+
else
|
|
531
|
+
error(code, "Failed to GET #{buri}: #{code} #{reason}")
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
annotation_graph
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
##
|
|
538
|
+
# Retrieve annotation for given annotation URI
|
|
539
|
+
#
|
|
540
|
+
# Returns: [code, reason, uri, annotation_graph]
|
|
541
|
+
def get_annotation(annotation_uri)
|
|
542
|
+
code, reason, headers, uri, annotation_graph = get_resource(annotation_uri)
|
|
543
|
+
[code, reason, uri, annotation_graph]
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
##
|
|
547
|
+
# Remove annotation at given annotation URI
|
|
548
|
+
#
|
|
549
|
+
# Returns: (code, reason)
|
|
550
|
+
def remove_annotation(annotation_uri)
|
|
551
|
+
code, reason = do_request("DELETE", annotation_uri)
|
|
552
|
+
if code == 204
|
|
553
|
+
[code, reason]
|
|
554
|
+
else
|
|
555
|
+
error(code, "Failed to DELETE annotation #{annotation_uri}: #{code} #{reason}")
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# -----------------------
|
|
560
|
+
# Folders
|
|
561
|
+
# -----------------------
|
|
562
|
+
|
|
563
|
+
##
|
|
564
|
+
# Returns [code, reason, headers, uri, folder_contents]
|
|
565
|
+
def get_folder(folder_uri)
|
|
566
|
+
code, reason, headers, uri, folder_contents = do_request_rdf("GET", folder_uri,
|
|
567
|
+
:accept => 'application/vnd.wf4ever.folder')
|
|
568
|
+
if code != 200
|
|
569
|
+
error(code, reason)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
[code, reason, headers, uri, folder_contents]
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
##
|
|
576
|
+
# +contents+ is an Array containing Hash elements, which must consist of a :uri and an optional :name.
|
|
577
|
+
# Example:
|
|
578
|
+
# folder_contents = [{:name => 'test_data.txt', :uri => 'http://www.example.com/ro/file1.txt'},
|
|
579
|
+
# {:uri => 'http://www.myexperiment.org/workflows/7'}]
|
|
580
|
+
# create_folder('ros/new_ro/', 'example_data', folder_contents)
|
|
581
|
+
#
|
|
582
|
+
# Returns [code, reason, uri, proxy_uri, folder_description_graph]
|
|
583
|
+
def create_folder(ro_uri, name, contents = [])
|
|
584
|
+
name << "/" unless name[-1] == "/" # Need trailing slash on folders...
|
|
585
|
+
code, reason, headers, uripath, folder_description = do_request_rdf("POST", ro_uri,
|
|
586
|
+
:body => create_folder_description(contents),
|
|
587
|
+
:headers => {"Slug" => name,
|
|
588
|
+
"Content-Type" => 'application/vnd.wf4ever.folder',},
|
|
589
|
+
:accept => 'application/vnd.wf4ever.folder')
|
|
590
|
+
|
|
591
|
+
if code == 201
|
|
592
|
+
uri = parse_links(headers)[RDF::ORE.proxyFor.to_s].first
|
|
593
|
+
[code, reason, uri, headers["location"], folder_description]
|
|
594
|
+
else
|
|
595
|
+
error(code, "Error creating folder: #{code} #{reason}")
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def add_folder_entry(folder_uri, resource_uri, resource_name = nil)
|
|
600
|
+
code, reason, headers, body= do_request("POST", folder_uri,
|
|
601
|
+
:body => create_folder_entry_description(resource_uri, resource_name),
|
|
602
|
+
:headers => {"Content-Type" => 'application/vnd.wf4ever.folderentry',})
|
|
603
|
+
if code == 201
|
|
604
|
+
[code, reason, headers["Location"], parse_links(headers)[RDF::ORE.proxyFor.to_s].first]
|
|
605
|
+
else
|
|
606
|
+
error(code, "Error adding resource to folder: #{code} #{reason}")
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
#--------
|
|
611
|
+
|
|
612
|
+
def delete_resource(resource_uri)
|
|
613
|
+
code, reason = do_request_follow_redirect("DELETE", resource_uri)
|
|
614
|
+
error(code, "Error deleting resource #{resource_uri}: #{code} #{reason}") unless [204,404].include?(code)
|
|
615
|
+
[code, reason]
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
private
|
|
619
|
+
|
|
620
|
+
def parseable?(content_type)
|
|
621
|
+
PARSEABLE_CONTENT_TYPES.include?(content_type.downcase)
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
##
|
|
625
|
+
# Takes +contents+, an Array containing Hash elements, which must consist of a :uri and an optional :name,
|
|
626
|
+
# and returns an RDF description of the folder contents.
|
|
627
|
+
def create_folder_description(contents)
|
|
628
|
+
body = %(
|
|
629
|
+
<rdf:RDF
|
|
630
|
+
xmlns:ore="#{RDF::ORE.to_uri.to_s}"
|
|
631
|
+
xmlns:rdf="#{RDF.to_uri.to_s}"
|
|
632
|
+
xmlns:ro="#{RDF::RO.to_uri.to_s}" >
|
|
633
|
+
<ro:Folder>
|
|
634
|
+
#{contents.collect {|r| "<ore:aggregates rdf:resource=\"#{r[:uri]}\" />" }.join("\n")}
|
|
635
|
+
</ro:Folder>
|
|
636
|
+
)
|
|
637
|
+
contents.each do |r|
|
|
638
|
+
if r[:name]
|
|
639
|
+
body << create_folder_entry_body(r[:uri], r[:name])
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
body << %(
|
|
643
|
+
</rdf:RDF>
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
body
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def create_folder_entry_description(uri, name = nil)
|
|
650
|
+
%(
|
|
651
|
+
<rdf:RDF
|
|
652
|
+
xmlns:ore="#{RDF::ORE.to_uri.to_s}"
|
|
653
|
+
xmlns:rdf="#{RDF.to_uri.to_s}"
|
|
654
|
+
xmlns:ro="#{RDF::RO.to_uri.to_s}" >
|
|
655
|
+
#{create_folder_entry_body(uri, name)}
|
|
656
|
+
</rdf:RDF>
|
|
657
|
+
)
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def create_folder_entry_body(uri, name = nil)
|
|
661
|
+
body = %(
|
|
662
|
+
<ro:FolderEntry>
|
|
663
|
+
)
|
|
664
|
+
body << "<ro:entryName>#{name}</ro:entryName>" if name
|
|
665
|
+
body << %(<ore:proxyFor rdf:resource="#{uri}" />
|
|
666
|
+
</ro:FolderEntry>
|
|
667
|
+
)
|
|
668
|
+
body
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
end
|
|
672
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'logger'
|
|
3
|
+
|
|
4
|
+
require 'rubygems'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'rdf'
|
|
7
|
+
require 'rdf/raptor'
|
|
8
|
+
|
|
9
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'namespaces'))
|
|
10
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'rdf_graph'))
|
|
11
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'session'))
|
|
12
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'helper'))
|
|
13
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'exceptions'))
|
|
14
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'research_object'))
|
|
15
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'annotation'))
|
|
16
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'resource'))
|
|
17
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'folder'))
|
|
18
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rosrs', 'folder_entry'))
|
|
19
|
+
|
|
20
|
+
|
data/test/helper.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'bundler'
|
|
3
|
+
begin
|
|
4
|
+
Bundler.setup(:default, :development)
|
|
5
|
+
rescue Bundler::BundlerError => e
|
|
6
|
+
$stderr.puts e.message
|
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
|
8
|
+
exit e.status_code
|
|
9
|
+
end
|
|
10
|
+
require 'test/unit'
|
|
11
|
+
|
|
12
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
13
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
14
|
+
require 'wf4ever/rosrs_client'
|
|
15
|
+
|
|
16
|
+
class Test::Unit::TestCase
|
|
17
|
+
end
|