wf4ever-rosrs-client 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|