openbel-api 0.4.0-java
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 +7 -0
- data/.gemspec +65 -0
- data/CHANGELOG.md +22 -0
- data/INSTALL.md +19 -0
- data/INSTALL_RUBY.md +107 -0
- data/LICENSE +191 -0
- data/README.md +208 -0
- data/app/openbel/api/app.rb +83 -0
- data/app/openbel/api/config.rb +45 -0
- data/app/openbel/api/config.ru +3 -0
- data/app/openbel/api/helpers/pager.rb +109 -0
- data/app/openbel/api/middleware/auth.rb +112 -0
- data/app/openbel/api/resources/adapters/basic_json.rb +52 -0
- data/app/openbel/api/resources/annotation.rb +141 -0
- data/app/openbel/api/resources/base.rb +16 -0
- data/app/openbel/api/resources/completion.rb +89 -0
- data/app/openbel/api/resources/evidence.rb +115 -0
- data/app/openbel/api/resources/evidence_transform.rb +143 -0
- data/app/openbel/api/resources/function.rb +98 -0
- data/app/openbel/api/resources/match_result.rb +79 -0
- data/app/openbel/api/resources/namespace.rb +174 -0
- data/app/openbel/api/routes/annotations.rb +168 -0
- data/app/openbel/api/routes/authenticate.rb +108 -0
- data/app/openbel/api/routes/base.rb +326 -0
- data/app/openbel/api/routes/datasets.rb +519 -0
- data/app/openbel/api/routes/evidence.rb +330 -0
- data/app/openbel/api/routes/expressions.rb +560 -0
- data/app/openbel/api/routes/functions.rb +41 -0
- data/app/openbel/api/routes/namespaces.rb +382 -0
- data/app/openbel/api/routes/root.rb +39 -0
- data/app/openbel/api/schemas.rb +34 -0
- data/app/openbel/api/schemas/annotation_collection.schema.json +20 -0
- data/app/openbel/api/schemas/annotation_resource.schema.json +36 -0
- data/app/openbel/api/schemas/annotation_value_collection.schema.json +21 -0
- data/app/openbel/api/schemas/annotation_value_resource.schema.json +35 -0
- data/app/openbel/api/schemas/completion_collection.schema.json +21 -0
- data/app/openbel/api/schemas/completion_resource.schema.json +146 -0
- data/app/openbel/api/schemas/evidence.schema.json +198 -0
- data/app/openbel/api/schemas/evidence_collection.schema.json +98 -0
- data/app/openbel/api/schemas/evidence_resource.schema.json +29 -0
- data/app/openbel/api/schemas/namespace_value_collection.schema.json +21 -0
- data/app/openbel/api/schemas/namespace_value_resource.schema.json +43 -0
- data/app/openbel/api/util.rb +11 -0
- data/bin/openbel-api +78 -0
- data/bin/openbel-config +46 -0
- data/config/async_evidence.rb +12 -0
- data/config/async_jena.rb +14 -0
- data/config/config.yml +31 -0
- data/config/server_config.rb +184 -0
- data/lib/openbel/api/cache/cache.rb +30 -0
- data/lib/openbel/api/config/config.rb +33 -0
- data/lib/openbel/api/evidence/api.rb +39 -0
- data/lib/openbel/api/evidence/facet_api.rb +18 -0
- data/lib/openbel/api/evidence/facet_filter.rb +83 -0
- data/lib/openbel/api/evidence/mongo.rb +247 -0
- data/lib/openbel/api/evidence/mongo_facet.rb +105 -0
- data/lib/openbel/api/helpers/dependency_graph.rb +52 -0
- data/lib/openbel/api/model/rdf_resource.rb +74 -0
- data/lib/openbel/api/plugin/cache/kyotocabinet.rb +85 -0
- data/lib/openbel/api/plugin/configure_plugins.rb +97 -0
- data/lib/openbel/api/plugin/evidence/evidence.rb +58 -0
- data/lib/openbel/api/plugin/plugin.rb +99 -0
- data/lib/openbel/api/plugin/plugin_manager.rb +20 -0
- data/lib/openbel/api/plugin/plugin_repository.rb +60 -0
- data/lib/openbel/api/storage/cache_proxy.rb +74 -0
- data/lib/openbel/api/storage/triple_storage.rb +43 -0
- metadata +379 -0
@@ -0,0 +1,326 @@
|
|
1
|
+
require 'bel'
|
2
|
+
require 'json_schema'
|
3
|
+
require 'multi_json'
|
4
|
+
require_relative '../resources/annotation'
|
5
|
+
require_relative '../resources/completion'
|
6
|
+
require_relative '../resources/evidence'
|
7
|
+
require_relative '../resources/function'
|
8
|
+
require_relative '../resources/match_result'
|
9
|
+
require_relative '../resources/namespace'
|
10
|
+
require_relative '../schemas'
|
11
|
+
|
12
|
+
module OpenBEL
|
13
|
+
module Routes
|
14
|
+
|
15
|
+
class Base < Sinatra::Application
|
16
|
+
include OpenBEL::Resource::Annotations
|
17
|
+
include OpenBEL::Resource::Evidence
|
18
|
+
include OpenBEL::Resource::Expressions
|
19
|
+
include OpenBEL::Resource::Functions
|
20
|
+
include OpenBEL::Resource::MatchResults
|
21
|
+
include OpenBEL::Resource::Namespaces
|
22
|
+
include OpenBEL::Schemas
|
23
|
+
|
24
|
+
DEFAULT_CONTENT_TYPE = 'application/hal+json'
|
25
|
+
SPOKEN_CONTENT_TYPES = %w[application/hal+json application/json]
|
26
|
+
SPOKEN_CONTENT_TYPES.concat(
|
27
|
+
BEL::Translator.plugins.values.flat_map { |p| p.media_types.map(&:to_s) }
|
28
|
+
)
|
29
|
+
|
30
|
+
SCHEMA_BASE_URL = 'http://next.belframework.org/schemas/'
|
31
|
+
RESOURCE_SERIALIZERS = {
|
32
|
+
:annotation => AnnotationResourceSerializer,
|
33
|
+
:annotation_collection => AnnotationCollectionSerializer,
|
34
|
+
:annotation_value => AnnotationValueResourceSerializer,
|
35
|
+
:completion => CompletionResourceSerializer,
|
36
|
+
:completion_collection => CompletionCollectionSerializer,
|
37
|
+
:function => FunctionResourceSerializer,
|
38
|
+
:function_collection => FunctionCollectionSerializer,
|
39
|
+
:match_result => MatchResultResourceSerializer,
|
40
|
+
:match_result_collection => MatchResultCollectionSerializer,
|
41
|
+
:namespace => NamespaceResourceSerializer,
|
42
|
+
:namespace_collection => NamespaceCollectionSerializer,
|
43
|
+
:namespace_value => NamespaceValueResourceSerializer,
|
44
|
+
:namespace_value_collection => NamespaceValueCollectionSerializer,
|
45
|
+
:evidence => EvidenceSerializer,
|
46
|
+
:evidence_resource => EvidenceResourceSerializer,
|
47
|
+
:evidence_collection => EvidenceCollectionSerializer
|
48
|
+
}
|
49
|
+
|
50
|
+
disable :protection
|
51
|
+
|
52
|
+
before do
|
53
|
+
unless request.preferred_type(SPOKEN_CONTENT_TYPES)
|
54
|
+
halt 406
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
helpers do
|
59
|
+
|
60
|
+
def as_bool(value)
|
61
|
+
value == nil ? false : value.to_s =~ (/^(true|t|yes|y|1|on)$/i)
|
62
|
+
end
|
63
|
+
|
64
|
+
def wildcard_match(match)
|
65
|
+
match.to_s.split(/\W/).map { |v| "*#{v}*"}.join(' ')
|
66
|
+
end
|
67
|
+
|
68
|
+
def request_headers
|
69
|
+
env.inject({}) { |hdrs, (k,v)|
|
70
|
+
hdrs[$1.downcase] = v if k =~ /^http_(.*)/i
|
71
|
+
hdrs
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def base_url
|
76
|
+
env['HTTP_X_REAL_BASE_URL'] ||
|
77
|
+
"#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def url
|
81
|
+
env['HTTP_X_REAL_URL'] ||
|
82
|
+
"#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}/#{env['PATH_INFO']}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def schema_url(name)
|
86
|
+
SCHEMA_BASE_URL + "#{name}.schema.json"
|
87
|
+
end
|
88
|
+
|
89
|
+
def validate_media_type!(content_type, options = {})
|
90
|
+
ctype = request.content_type
|
91
|
+
valid = ctype.start_with? content_type
|
92
|
+
if options[:profile]
|
93
|
+
valid &= (%r{profile=#{options[:profile]}} =~ ctype)
|
94
|
+
end
|
95
|
+
|
96
|
+
unless valid
|
97
|
+
halt(
|
98
|
+
415,
|
99
|
+
{
|
100
|
+
'Content-Type' => 'application/json'
|
101
|
+
},
|
102
|
+
render_json({
|
103
|
+
:status => 415,
|
104
|
+
:msg => "Invalid media type #{ctype} (profile: #{options[:profile]})"
|
105
|
+
})
|
106
|
+
)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def read_evidence
|
111
|
+
halt 415 unless ::BEL.translator(request.media_type)
|
112
|
+
::BEL.evidence(request.body, request.media_type, :symbolize_keys => true)
|
113
|
+
end
|
114
|
+
|
115
|
+
def read_json
|
116
|
+
request.body.rewind
|
117
|
+
begin
|
118
|
+
MultiJson.load(request.body.read, :symbolize_keys => true)
|
119
|
+
rescue MultiJson::ParseError => ex
|
120
|
+
halt(
|
121
|
+
400,
|
122
|
+
{
|
123
|
+
'Content-Type' => 'application/json'
|
124
|
+
},
|
125
|
+
render_json({
|
126
|
+
:status => 400,
|
127
|
+
:msg => 'Invalid JSON body.',
|
128
|
+
:detail => ex.cause.to_s
|
129
|
+
})
|
130
|
+
)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def read_filter(filter_json)
|
135
|
+
begin
|
136
|
+
MultiJson.load filter_json
|
137
|
+
rescue MultiJson::ParseError => ex
|
138
|
+
halt(
|
139
|
+
400,
|
140
|
+
{
|
141
|
+
'Content-Type' => 'application/json'
|
142
|
+
},
|
143
|
+
render_json({
|
144
|
+
:status => 400,
|
145
|
+
:msg => "Invalid JSON filter: #{filter_json}.",
|
146
|
+
:detail => ex.cause.to_s
|
147
|
+
})
|
148
|
+
)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def render_json(obj, media_type = 'application/hal+json', profile = nil)
|
153
|
+
ctype =
|
154
|
+
if profile
|
155
|
+
"#{media_type}; profile=#{profile}"
|
156
|
+
else
|
157
|
+
media_type
|
158
|
+
end
|
159
|
+
response.headers['Content-Type'] = ctype.to_s
|
160
|
+
MultiJson.dump obj
|
161
|
+
end
|
162
|
+
|
163
|
+
def validate_schema(data, type)
|
164
|
+
self.validate(data, type)
|
165
|
+
end
|
166
|
+
|
167
|
+
def render_resource(obj, type, options = {})
|
168
|
+
media_type = 'application/hal+json'
|
169
|
+
resource_context = {
|
170
|
+
:base_url => base_url,
|
171
|
+
:url => url
|
172
|
+
}.merge(options)
|
173
|
+
|
174
|
+
type_class = type.to_s.split('_').map(&:capitalize).join
|
175
|
+
type_serializer = self.class.const_get("#{type_class}Serializer")
|
176
|
+
resource_serializer = self.class.const_get("#{type_class}ResourceSerializer")
|
177
|
+
|
178
|
+
adapter = Oat::Adapters::HAL
|
179
|
+
if options[:adapter]
|
180
|
+
adapter = options[:adapter]
|
181
|
+
end
|
182
|
+
|
183
|
+
resource = resource_serializer.new(
|
184
|
+
type_serializer.new(obj, resource_context, adapter).to_hash,
|
185
|
+
resource_context,
|
186
|
+
adapter
|
187
|
+
).to_hash
|
188
|
+
|
189
|
+
render_json(resource.to_hash, media_type)
|
190
|
+
end
|
191
|
+
|
192
|
+
def render_collection(collection, type, options = {})
|
193
|
+
media_type = 'application/hal+json'
|
194
|
+
resource_context = {
|
195
|
+
:base_url => base_url,
|
196
|
+
:url => url
|
197
|
+
}.merge(options)
|
198
|
+
|
199
|
+
type_class = type.to_s.split('_').map(&:capitalize).join
|
200
|
+
serializer = self.class.const_get("#{type_class}Serializer")
|
201
|
+
type_serializer = self.class.const_get("#{type_class}ResourceSerializer")
|
202
|
+
resource_serializer = self.class.const_get("#{type_class}CollectionSerializer")
|
203
|
+
|
204
|
+
adapter = Oat::Adapters::HAL
|
205
|
+
if options[:adapter]
|
206
|
+
adapter = options[:adapter]
|
207
|
+
end
|
208
|
+
|
209
|
+
resource = resource_serializer.new(
|
210
|
+
collection.map { |obj|
|
211
|
+
single = serializer.new(obj, resource_context, adapter).to_hash
|
212
|
+
type_serializer.new(single, resource_context, adapter).to_hash
|
213
|
+
},
|
214
|
+
resource_context,
|
215
|
+
adapter
|
216
|
+
).to_hash
|
217
|
+
|
218
|
+
render_json(resource.to_hash, media_type)
|
219
|
+
end
|
220
|
+
|
221
|
+
def render(obj, type, options = {})
|
222
|
+
media_type = 'application/hal+json'
|
223
|
+
resource_context = {
|
224
|
+
:base_url => base_url,
|
225
|
+
:url => url
|
226
|
+
}.merge(options)
|
227
|
+
|
228
|
+
serializer_class = RESOURCE_SERIALIZERS[type]
|
229
|
+
unless serializer_class
|
230
|
+
raise NotImplementedError.new("Cannot serialize the #{type} resource.")
|
231
|
+
end
|
232
|
+
|
233
|
+
adapter = Oat::Adapters::HAL
|
234
|
+
|
235
|
+
# XXX hack to fix OpenBEL/openbel-server#33
|
236
|
+
if options[:as_array]
|
237
|
+
render_json(
|
238
|
+
{
|
239
|
+
type => [serializer_class.new(obj, resource_context, adapter).to_hash]
|
240
|
+
},
|
241
|
+
media_type
|
242
|
+
)
|
243
|
+
else
|
244
|
+
render_json(
|
245
|
+
serializer_class.new(obj, resource_context, adapter).to_hash,
|
246
|
+
media_type
|
247
|
+
)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def stream_resource_collection(type, collection, facets, options = {})
|
252
|
+
serializer_class = RESOURCE_SERIALIZERS[type]
|
253
|
+
unless serializer_class
|
254
|
+
raise NotImplementedError.new("Cannot serialize the #{type} resource collection.")
|
255
|
+
end
|
256
|
+
|
257
|
+
media_type = resolve_supported_content_type(request)
|
258
|
+
resource_context = {
|
259
|
+
:base_url => base_url,
|
260
|
+
:url => url
|
261
|
+
}.merge(options)
|
262
|
+
|
263
|
+
adapter = Oat::Adapters::HAL
|
264
|
+
|
265
|
+
response.headers['Content-Type'] = media_type
|
266
|
+
collection_enum = collection.to_enum
|
267
|
+
|
268
|
+
stream do |response|
|
269
|
+
response << %Q({"#{type}": [)
|
270
|
+
item = collection_enum.next
|
271
|
+
response << render_json(
|
272
|
+
serializer_class.new(item, resource_context, adapter).to_hash,
|
273
|
+
media_type
|
274
|
+
)
|
275
|
+
while true
|
276
|
+
begin
|
277
|
+
item = collection_enum.next
|
278
|
+
response << (',' + render_json(
|
279
|
+
serializer_class.new(item, resource_context, adapter).to_hash,
|
280
|
+
media_type
|
281
|
+
))
|
282
|
+
rescue StopIteration
|
283
|
+
break
|
284
|
+
end
|
285
|
+
end
|
286
|
+
response << ']'
|
287
|
+
|
288
|
+
if facets
|
289
|
+
response << ',"facets": ['
|
290
|
+
|
291
|
+
facet_enum = facets.to_enum
|
292
|
+
facet_item = facet_enum.next
|
293
|
+
filter = MultiJson.load(facet_item['_id'])
|
294
|
+
response << MultiJson.dump({
|
295
|
+
:category => filter['category'].to_sym,
|
296
|
+
:name => filter['name'].to_sym,
|
297
|
+
:value => filter['value'],
|
298
|
+
:filter => facet_item['_id'],
|
299
|
+
:count => facet_item['count']
|
300
|
+
})
|
301
|
+
while true
|
302
|
+
begin
|
303
|
+
facet_item = facet_enum.next
|
304
|
+
filter = MultiJson.load(facet_item['_id'])
|
305
|
+
response << (',' + MultiJson.dump({
|
306
|
+
:category => filter['category'].to_sym,
|
307
|
+
:name => filter['name'].to_sym,
|
308
|
+
:value => filter['value'],
|
309
|
+
:filter => facet_item['_id'],
|
310
|
+
:count => facet_item['count']
|
311
|
+
}))
|
312
|
+
rescue StopIteration
|
313
|
+
break
|
314
|
+
end
|
315
|
+
end
|
316
|
+
response << ']'
|
317
|
+
end
|
318
|
+
|
319
|
+
response << '}'
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
@@ -0,0 +1,519 @@
|
|
1
|
+
require 'bel'
|
2
|
+
require 'rdf'
|
3
|
+
require 'cgi'
|
4
|
+
require 'multi_json'
|
5
|
+
require 'openbel/api/evidence/mongo'
|
6
|
+
require 'openbel/api/evidence/facet_filter'
|
7
|
+
require_relative '../resources/evidence_transform'
|
8
|
+
require_relative '../helpers/pager'
|
9
|
+
|
10
|
+
module OpenBEL
|
11
|
+
module Routes
|
12
|
+
|
13
|
+
class Datasets < Base
|
14
|
+
include OpenBEL::Evidence::FacetFilter
|
15
|
+
include OpenBEL::Resource::Evidence
|
16
|
+
include OpenBEL::Helpers
|
17
|
+
|
18
|
+
DEFAULT_TYPE = 'application/hal+json'
|
19
|
+
|
20
|
+
ACCEPTED_TYPES = {
|
21
|
+
:bel => 'application/bel',
|
22
|
+
:xml => 'application/xml',
|
23
|
+
:xbel => 'application/xml',
|
24
|
+
:json => 'application/json',
|
25
|
+
}
|
26
|
+
|
27
|
+
def initialize(app)
|
28
|
+
super
|
29
|
+
|
30
|
+
# Evidence API using Mongo.
|
31
|
+
@api = OpenBEL::Evidence::Evidence.new(
|
32
|
+
:host => OpenBEL::Settings[:evidence_store][:mongo][:host],
|
33
|
+
:port => OpenBEL::Settings[:evidence_store][:mongo][:port],
|
34
|
+
:database => OpenBEL::Settings[:evidence_store][:mongo][:database]
|
35
|
+
)
|
36
|
+
|
37
|
+
# RdfRepository using Jena.
|
38
|
+
@rr = BEL::RdfRepository.plugins[:jena].create_repository(
|
39
|
+
:tdb_directory => OpenBEL::Settings[:resource_rdf][:jena][:tdb_directory]
|
40
|
+
)
|
41
|
+
|
42
|
+
# Load RDF monkeypatches.
|
43
|
+
BEL::Translator.plugins[:rdf].create_translator
|
44
|
+
|
45
|
+
# Annotations using RdfRepository
|
46
|
+
annotations = BEL::Resource::Annotations.new(@rr)
|
47
|
+
@annotation_transform = AnnotationTransform.new(annotations)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Hang on to the Rack IO in order to do unbuffered reads.
|
51
|
+
# use Rack::Config do |env|
|
52
|
+
# env['rack.input'], env['data.input'] = StringIO.new, env['rack.input']
|
53
|
+
# end
|
54
|
+
|
55
|
+
configure do
|
56
|
+
ACCEPTED_TYPES.each do |ext, mime_type|
|
57
|
+
mime_type(ext, mime_type)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
helpers do
|
62
|
+
|
63
|
+
def check_dataset(io, type)
|
64
|
+
begin
|
65
|
+
evidence = BEL.evidence(io, type).each.first
|
66
|
+
|
67
|
+
unless evidence
|
68
|
+
halt(
|
69
|
+
400,
|
70
|
+
{ 'Content-Type' => 'application/json' },
|
71
|
+
render_json({ :status => 400, :msg => 'No BEL evidence was provided. Evidence is required to infer dataset information.' })
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
void_dataset_uri = RDF::URI("#{base_url}/api/datasets/#{self.generate_uuid}")
|
76
|
+
|
77
|
+
void_dataset = evidence.to_void_dataset(void_dataset_uri)
|
78
|
+
unless void_dataset
|
79
|
+
halt(
|
80
|
+
400,
|
81
|
+
{ 'Content-Type' => 'application/json' },
|
82
|
+
render_json({ :status => 400, :msg => 'The dataset document does not contain a document header.' })
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
identifier_statement = void_dataset.query(
|
87
|
+
RDF::Statement.new(void_dataset_uri, RDF::DC.identifier, nil)
|
88
|
+
).to_a.first
|
89
|
+
unless identifier_statement
|
90
|
+
halt(
|
91
|
+
400,
|
92
|
+
{ 'Content-Type' => 'application/json' },
|
93
|
+
render_json(
|
94
|
+
{
|
95
|
+
:status => 400,
|
96
|
+
:msg => 'The dataset document does not contain the Name or Version needed to build an identifier.'
|
97
|
+
}
|
98
|
+
)
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
datasets = @rr.query_pattern(RDF::Statement.new(nil, RDF.type, RDF::VOID.Dataset))
|
103
|
+
existing_dataset = datasets.find { |dataset_statement|
|
104
|
+
@rr.has_statement?(
|
105
|
+
RDF::Statement.new(dataset_statement.subject, RDF::DC.identifier, identifier_statement.object)
|
106
|
+
)
|
107
|
+
}
|
108
|
+
|
109
|
+
if existing_dataset
|
110
|
+
dataset_uri = existing_dataset.subject.to_s
|
111
|
+
headers 'Location' => dataset_uri
|
112
|
+
halt(
|
113
|
+
409,
|
114
|
+
{ 'Content-Type' => 'application/json' },
|
115
|
+
render_json(
|
116
|
+
{
|
117
|
+
:status => 409,
|
118
|
+
:msg => %Q{The dataset document matches an existing dataset resource by identifier "#{identifier_statement.object}".},
|
119
|
+
:location => dataset_uri
|
120
|
+
}
|
121
|
+
)
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
[void_dataset_uri, void_dataset]
|
126
|
+
ensure
|
127
|
+
io.rewind
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def dataset_exists?(uri)
|
132
|
+
@rr.has_statement?(
|
133
|
+
RDF::Statement.new(uri, RDF.type, RDF::VOID.Dataset)
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
137
|
+
def retrieve_dataset(uri)
|
138
|
+
dataset = {}
|
139
|
+
identifier = @rr.query(
|
140
|
+
RDF::Statement.new(uri, RDF::DC.identifier, nil)
|
141
|
+
).first
|
142
|
+
dataset[:identifier] = identifier.object.to_s if identifier
|
143
|
+
|
144
|
+
title = @rr.query(
|
145
|
+
RDF::Statement.new(uri, RDF::DC.title, nil)
|
146
|
+
).first
|
147
|
+
dataset[:title] = title.object.to_s if title
|
148
|
+
|
149
|
+
description = @rr.query(
|
150
|
+
RDF::Statement.new(uri, RDF::DC.description, nil)
|
151
|
+
).first
|
152
|
+
dataset[:description] = description.object.to_s if description
|
153
|
+
|
154
|
+
waiver = @rr.query(
|
155
|
+
RDF::Statement.new(uri, RDF::URI('http://vocab.org/waiver/terms/waiver'), nil)
|
156
|
+
).first
|
157
|
+
dataset[:waiver] = waiver.object.to_s if waiver
|
158
|
+
|
159
|
+
creator = @rr.query(
|
160
|
+
RDF::Statement.new(uri, RDF::DC.creator, nil)
|
161
|
+
).first
|
162
|
+
dataset[:creator] = creator.object.to_s if creator
|
163
|
+
|
164
|
+
license = @rr.query(
|
165
|
+
RDF::Statement.new(uri, RDF::DC.license, nil)
|
166
|
+
).first
|
167
|
+
dataset[:license] = license.object.to_s if license
|
168
|
+
|
169
|
+
publisher = @rr.query(
|
170
|
+
RDF::Statement.new(uri, RDF::DC.publisher, nil)
|
171
|
+
).first
|
172
|
+
if publisher
|
173
|
+
publisher.object
|
174
|
+
contact_info = @rr.query(
|
175
|
+
RDF::Statement.new(publisher.object, RDF::FOAF.mbox, nil)
|
176
|
+
).first
|
177
|
+
dataset[:contact_info] = contact_info.object.to_s if contact_info
|
178
|
+
end
|
179
|
+
|
180
|
+
dataset
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
options '/api/datasets' do
|
185
|
+
response.headers['Allow'] = 'OPTIONS,POST,GET'
|
186
|
+
status 200
|
187
|
+
end
|
188
|
+
|
189
|
+
options '/api/datasets/:id' do
|
190
|
+
response.headers['Allow'] = 'OPTIONS,GET,PUT,DELETE'
|
191
|
+
status 200
|
192
|
+
end
|
193
|
+
|
194
|
+
post '/api/datasets' do
|
195
|
+
if request.media_type == 'multipart/form-data' && params['file']
|
196
|
+
io, filename, type = params['file'].values_at(:tempfile, :filename, :type)
|
197
|
+
unless ACCEPTED_TYPES.values.include?(type)
|
198
|
+
type = mime_type(File.extname(filename))
|
199
|
+
end
|
200
|
+
|
201
|
+
halt(
|
202
|
+
415,
|
203
|
+
{ 'Content-Type' => 'application/json' },
|
204
|
+
render_json({
|
205
|
+
:status => 415,
|
206
|
+
:msg => %Q{
|
207
|
+
[Form data] Do not support content type for "#{type || filename}" when processing datasets from the "file" form parameter.
|
208
|
+
The following content types are allowed: #{ACCEPTED_TYPES.values.join(', ')}. The "file" form parameter type can also be inferred by the following file extensions: #{ACCEPTED_TYPES.keys.join(', ')}} })
|
209
|
+
) unless ACCEPTED_TYPES.values.include?(type)
|
210
|
+
elsif ACCEPTED_TYPES.values.include?(request.media_type)
|
211
|
+
type = request.media_type
|
212
|
+
io = request.body
|
213
|
+
|
214
|
+
halt(
|
215
|
+
415,
|
216
|
+
{ 'Content-Type' => 'application/json' },
|
217
|
+
render_json({
|
218
|
+
:status => 415,
|
219
|
+
:msg => %Q{[POST data] Do not support content type #{type} when processing datasets. The following content types
|
220
|
+
are allowed in the "Content-Type" header: #{ACCEPTED_TYPES.values.join(', ')}} })
|
221
|
+
) unless ACCEPTED_TYPES.values.include?(type)
|
222
|
+
else
|
223
|
+
halt(
|
224
|
+
400,
|
225
|
+
{ 'Content-Type' => 'application/json' },
|
226
|
+
render_json({
|
227
|
+
:status => 400,
|
228
|
+
:msg => %Q{Please POST data using a supported "Content-Type" or a "file" parameter using
|
229
|
+
the "multipart/form-data" content type. Allowed dataset content types are: #{ACCEPTED_TYPES.values.join(', ')}} })
|
230
|
+
)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Check dataset in request for suitability and conflict with existing resources.
|
234
|
+
void_dataset_uri, void_dataset = check_dataset(io, type)
|
235
|
+
|
236
|
+
# Create dataset in RDF.
|
237
|
+
@rr.insert_statements(void_dataset)
|
238
|
+
|
239
|
+
dataset = retrieve_dataset(void_dataset_uri)
|
240
|
+
|
241
|
+
# Add slices of read evidence objects; save to Mongo and RDF.
|
242
|
+
BEL.evidence(io, type).each.lazy.each_slice(500) do |slice|
|
243
|
+
slice.map! do |ev|
|
244
|
+
# Standardize annotations from experiment_context.
|
245
|
+
@annotation_transform.transform_evidence!(ev, base_url)
|
246
|
+
|
247
|
+
# Add filterable metadata field for dataset identifier.
|
248
|
+
ev.metadata[:dataset] = dataset[:identifier]
|
249
|
+
|
250
|
+
facets = map_evidence_facets(ev)
|
251
|
+
ev.bel_statement = ev.bel_statement.to_s
|
252
|
+
hash = ev.to_h
|
253
|
+
hash[:facets] = facets
|
254
|
+
|
255
|
+
# Create dataset field for efficient removal.
|
256
|
+
hash[:_dataset] = dataset[:identifier]
|
257
|
+
hash
|
258
|
+
end
|
259
|
+
|
260
|
+
_ids = @api.create_evidence(slice)
|
261
|
+
|
262
|
+
dataset_parts = _ids.map { |object_id|
|
263
|
+
RDF::Statement.new(void_dataset_uri, RDF::DC.hasPart, object_id.to_s)
|
264
|
+
}
|
265
|
+
@rr.insert_statements(dataset_parts)
|
266
|
+
end
|
267
|
+
|
268
|
+
status 201
|
269
|
+
headers 'Location' => void_dataset_uri.to_s
|
270
|
+
end
|
271
|
+
|
272
|
+
get '/api/datasets/:id' do
|
273
|
+
id = params[:id]
|
274
|
+
void_dataset_uri = RDF::URI("#{base_url}/api/datasets/#{id}")
|
275
|
+
halt 404 unless dataset_exists?(void_dataset_uri)
|
276
|
+
|
277
|
+
status 200
|
278
|
+
render_json({
|
279
|
+
:dataset => retrieve_dataset(void_dataset_uri),
|
280
|
+
:_links => {
|
281
|
+
:self => {
|
282
|
+
:type => 'dataset',
|
283
|
+
:href => void_dataset_uri.to_s
|
284
|
+
},
|
285
|
+
:evidence_collection => {
|
286
|
+
:type => 'evidence_collection',
|
287
|
+
:href => "#{base_url}/api/datasets/#{id}/evidence"
|
288
|
+
}
|
289
|
+
}
|
290
|
+
})
|
291
|
+
end
|
292
|
+
|
293
|
+
get '/api/datasets/:id/evidence' do
|
294
|
+
id = params[:id]
|
295
|
+
void_dataset_uri = RDF::URI("#{base_url}/api/datasets/#{id}")
|
296
|
+
halt 404 unless dataset_exists?(void_dataset_uri)
|
297
|
+
|
298
|
+
dataset = retrieve_dataset(void_dataset_uri)
|
299
|
+
|
300
|
+
start = (params[:start] || 0).to_i
|
301
|
+
size = (params[:size] || 0).to_i
|
302
|
+
faceted = as_bool(params[:faceted])
|
303
|
+
max_values_per_facet = (params[:max_values_per_facet] || 0).to_i
|
304
|
+
|
305
|
+
# check filters
|
306
|
+
filters = []
|
307
|
+
filter_params = CGI::parse(env["QUERY_STRING"])['filter']
|
308
|
+
filter_params.each do |filter|
|
309
|
+
filter = read_filter(filter)
|
310
|
+
halt 400 unless ['category', 'name', 'value'].all? { |f| filter.include? f}
|
311
|
+
|
312
|
+
if filter['category'] == 'fts' && filter['name'] == 'search'
|
313
|
+
unless filter['value'].to_s.length > 1
|
314
|
+
halt(
|
315
|
+
400,
|
316
|
+
{ 'Content-Type' => 'application/json' },
|
317
|
+
render_json({
|
318
|
+
:status => 400,
|
319
|
+
:msg => 'Full-text search filter values must be larger than one.'
|
320
|
+
})
|
321
|
+
)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# Remove dataset filters since we're filtering a specific one already.
|
326
|
+
next if filter.values_at('category', 'name') == ['metadata', 'dataset']
|
327
|
+
|
328
|
+
filters << filter
|
329
|
+
end
|
330
|
+
|
331
|
+
collection_total = @api.count_evidence
|
332
|
+
filtered_total = @api.count_evidence(filters)
|
333
|
+
page_results = @api.find_dataset_evidence(dataset, filters, start, size, faceted)
|
334
|
+
|
335
|
+
accept_type = request.accept.find { |accept_entry|
|
336
|
+
ACCEPTED_TYPES.values.include?(accept_entry.to_s)
|
337
|
+
}
|
338
|
+
accept_type ||= DEFAULT_TYPE
|
339
|
+
|
340
|
+
if params[:format]
|
341
|
+
translator = BEL::Translator.plugins[params[:format].to_sym]
|
342
|
+
halt 501 if !translator || translator.id == :rdf
|
343
|
+
accept_type = [translator.media_types].flatten.first
|
344
|
+
end
|
345
|
+
|
346
|
+
if accept_type == DEFAULT_TYPE
|
347
|
+
evidence = page_results[:cursor].map { |item|
|
348
|
+
item.delete('facets')
|
349
|
+
item
|
350
|
+
}.to_a
|
351
|
+
|
352
|
+
facets = page_results[:facets]
|
353
|
+
|
354
|
+
halt 404 if evidence.empty?
|
355
|
+
|
356
|
+
pager = Pager.new(start, size, filtered_total)
|
357
|
+
|
358
|
+
options = {
|
359
|
+
:start => start,
|
360
|
+
:size => size,
|
361
|
+
:filters => filter_params,
|
362
|
+
:metadata => {
|
363
|
+
:collection_paging => {
|
364
|
+
:total => collection_total,
|
365
|
+
:total_filtered => pager.total_size,
|
366
|
+
:total_pages => pager.total_pages,
|
367
|
+
:current_page => pager.current_page,
|
368
|
+
:current_page_size => evidence.size,
|
369
|
+
}
|
370
|
+
}
|
371
|
+
}
|
372
|
+
|
373
|
+
if facets
|
374
|
+
# group by category/name
|
375
|
+
hashed_values = Hash.new { |hash, key| hash[key] = [] }
|
376
|
+
facets.each { |facet|
|
377
|
+
filter = read_filter(facet['_id'])
|
378
|
+
category, name = filter.values_at('category', 'name')
|
379
|
+
next if !category || !name
|
380
|
+
|
381
|
+
key = [category.to_sym, name.to_sym]
|
382
|
+
facet_obj = {
|
383
|
+
:value => filter['value'],
|
384
|
+
:filter => facet['_id'],
|
385
|
+
:count => facet['count']
|
386
|
+
}
|
387
|
+
hashed_values[key] << facet_obj
|
388
|
+
}
|
389
|
+
|
390
|
+
if max_values_per_facet == 0
|
391
|
+
facet_hashes = hashed_values.map { |(category, name), value_objects|
|
392
|
+
{
|
393
|
+
:category => category,
|
394
|
+
:name => name,
|
395
|
+
:values => value_objects
|
396
|
+
}
|
397
|
+
}
|
398
|
+
else
|
399
|
+
facet_hashes = hashed_values.map { |(category, name), value_objects|
|
400
|
+
{
|
401
|
+
:category => category,
|
402
|
+
:name => name,
|
403
|
+
:values => value_objects.take(max_values_per_facet)
|
404
|
+
}
|
405
|
+
}
|
406
|
+
end
|
407
|
+
|
408
|
+
options[:facets] = facet_hashes
|
409
|
+
end
|
410
|
+
|
411
|
+
# pager links
|
412
|
+
options[:previous_page] = pager.previous_page
|
413
|
+
options[:next_page] = pager.next_page
|
414
|
+
|
415
|
+
render_collection(evidence, :evidence, options)
|
416
|
+
else
|
417
|
+
out_translator = BEL.translator(accept_type)
|
418
|
+
extension = ACCEPTED_TYPES.key(accept_type.to_s)
|
419
|
+
|
420
|
+
response.headers['Content-Type'] = accept_type
|
421
|
+
status 200
|
422
|
+
attachment "#{dataset[:identifier].gsub(/[^\w]/, '_')}.#{extension}"
|
423
|
+
stream :keep_open do |response|
|
424
|
+
cursor = page_results[:cursor]
|
425
|
+
json_evidence_enum = cursor.lazy.map { |evidence|
|
426
|
+
evidence.delete('facets')
|
427
|
+
evidence.delete('_id')
|
428
|
+
evidence.keys.each do |key|
|
429
|
+
evidence[key.to_sym] = evidence.delete(key)
|
430
|
+
end
|
431
|
+
BEL::Model::Evidence.create(evidence)
|
432
|
+
}
|
433
|
+
|
434
|
+
out_translator.write(json_evidence_enum) do |converted_evidence|
|
435
|
+
response << converted_evidence
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
get '/api/datasets' do
|
442
|
+
dataset_uris = @rr.query(
|
443
|
+
RDF::Statement.new(nil, RDF.type, RDF::VOID.Dataset)
|
444
|
+
).map { |statement|
|
445
|
+
statement.subject
|
446
|
+
}.to_a
|
447
|
+
halt 404 if dataset_uris.empty?
|
448
|
+
|
449
|
+
dataset_collection = dataset_uris.map { |uri|
|
450
|
+
{
|
451
|
+
:dataset => retrieve_dataset(uri),
|
452
|
+
:_links => {
|
453
|
+
:self => {
|
454
|
+
:type => 'dataset',
|
455
|
+
:href => uri.to_s
|
456
|
+
},
|
457
|
+
:evidence_collection => {
|
458
|
+
:type => 'evidence_collection',
|
459
|
+
:href => "#{uri}/evidence"
|
460
|
+
}
|
461
|
+
}
|
462
|
+
}
|
463
|
+
}
|
464
|
+
|
465
|
+
status 200
|
466
|
+
render_json({ :dataset_collection => dataset_collection })
|
467
|
+
end
|
468
|
+
|
469
|
+
delete '/api/datasets/:id' do
|
470
|
+
id = params[:id]
|
471
|
+
void_dataset_uri = RDF::URI("#{base_url}/api/datasets/#{id}")
|
472
|
+
halt 404 unless dataset_exists?(void_dataset_uri)
|
473
|
+
|
474
|
+
dataset = retrieve_dataset(void_dataset_uri)
|
475
|
+
@api.delete_dataset(dataset[:identifier])
|
476
|
+
@rr.delete_statement(RDF::Statement.new(void_dataset_uri, nil, nil))
|
477
|
+
|
478
|
+
status 202
|
479
|
+
end
|
480
|
+
|
481
|
+
delete '/api/datasets' do
|
482
|
+
datasets = @rr.query(
|
483
|
+
RDF::Statement.new(nil, RDF.type, RDF::VOID.Dataset)
|
484
|
+
).map { |stmt|
|
485
|
+
stmt.subject
|
486
|
+
}.to_a
|
487
|
+
halt 404 if datasets.empty?
|
488
|
+
|
489
|
+
datasets.each do |void_dataset_uri|
|
490
|
+
dataset = retrieve_dataset(void_dataset_uri)
|
491
|
+
@api.delete_dataset(dataset[:identifier])
|
492
|
+
@rr.delete_statement(RDF::Statement.new(void_dataset_uri, nil, nil))
|
493
|
+
end
|
494
|
+
|
495
|
+
status 202
|
496
|
+
end
|
497
|
+
|
498
|
+
private
|
499
|
+
|
500
|
+
unless self.methods.include?(:generate_uuid)
|
501
|
+
|
502
|
+
# Dynamically defines an efficient UUID method for the current ruby.
|
503
|
+
if RUBY_ENGINE =~ /^jruby/i
|
504
|
+
java_import 'java.util.UUID'
|
505
|
+
define_method(:generate_uuid) do
|
506
|
+
Java::JavaUtil::UUID.random_uuid.to_s
|
507
|
+
end
|
508
|
+
else
|
509
|
+
require 'uuid'
|
510
|
+
define_method(:generate_uuid) do
|
511
|
+
UUID.generate
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
# vim: ts=2 sw=2:
|
519
|
+
# encoding: utf-8
|