iq_triplestorage 0.1.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.
- data/.travis.yml +16 -0
- data/Gemfile +9 -0
- data/README.md +3 -0
- data/Rakefile +8 -0
- data/iq_triplestorage.gemspec +17 -0
- data/lib/iq_triplestorage.rb +3 -0
- data/lib/iq_triplestorage/virtuoso_adaptor.rb +105 -0
- data/test/virtuoso_test.rb +138 -0
- metadata +53 -0
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.expand_path("../lib/iq_triplestorage", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "iq_triplestorage"
|
5
|
+
s.version = IqTriplestorage::VERSION
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
|
8
|
+
s.summary = "IqTriplestorage - library for interacting with RDF triplestores / quadstores"
|
9
|
+
s.homepage = "http://github.com/innoq/iq_triplestorage"
|
10
|
+
s.rubyforge_project = s.name
|
11
|
+
s.authors = ["FND"]
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'base64'
|
3
|
+
require 'typhoeus'
|
4
|
+
|
5
|
+
module IqTriplestorage
|
6
|
+
class VirtuosoAdaptor
|
7
|
+
|
8
|
+
def initialize(host, port, username, password)
|
9
|
+
@host = host
|
10
|
+
@port = port
|
11
|
+
@username = username
|
12
|
+
@password = password
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset(uri)
|
16
|
+
return sparql_pull("CLEAR GRAPH <#{uri}>") # XXX: s/CLEAR/DROP/ was rejected (405)
|
17
|
+
end
|
18
|
+
|
19
|
+
# expects a hash of N-Triples by graph URI
|
20
|
+
def batch_update(triples_by_graph)
|
21
|
+
# apparently Virtuoso gets confused when mixing CLEAR and INSERT queries,
|
22
|
+
# so we have to do use separate requests
|
23
|
+
|
24
|
+
reset_queries = triples_by_graph.keys.map do |graph_uri|
|
25
|
+
"CLEAR GRAPH <#{graph_uri}>" # XXX: duplicates `reset`
|
26
|
+
end
|
27
|
+
success = sparql_query(reset_queries)
|
28
|
+
return false unless success
|
29
|
+
|
30
|
+
insert_queries = triples_by_graph.map do |graph_uri, ntriples|
|
31
|
+
"INSERT IN GRAPH <#{graph_uri}> {\n#{ntriples}\n}"
|
32
|
+
end
|
33
|
+
success = sparql_query(insert_queries)
|
34
|
+
|
35
|
+
return success
|
36
|
+
end
|
37
|
+
|
38
|
+
# uses push method if `rdf_data` is provided, pull otherwise
|
39
|
+
def update(uri, rdf_data=nil, content_type=nil)
|
40
|
+
reset(uri)
|
41
|
+
|
42
|
+
if rdf_data
|
43
|
+
res = sparql_push(uri, rdf_data.strip, content_type)
|
44
|
+
else
|
45
|
+
res = sparql_pull(%{LOAD "#{uri}" INTO GRAPH <#{uri}>})
|
46
|
+
end
|
47
|
+
|
48
|
+
return res
|
49
|
+
end
|
50
|
+
|
51
|
+
def sparql_push(uri, rdf_data, content_type)
|
52
|
+
raise TypeError, "missing content type" unless content_type
|
53
|
+
|
54
|
+
filename = uri.gsub(/[^0-9A-Za-z]/, "_") # XXX: too simplistic?
|
55
|
+
path = "/DAV/home/#{@username}/rdf_sink/#{filename}"
|
56
|
+
|
57
|
+
auth = Base64.encode64([@username, @password].join(":")).strip
|
58
|
+
headers = {
|
59
|
+
"Authorization" => "Basic #{auth}", # XXX: seems like this should be built into Typhoeus!?
|
60
|
+
"Content-Type" => content_type
|
61
|
+
}
|
62
|
+
res = Typhoeus::Request.put("#{@host}:#{@port}#{path}",
|
63
|
+
:headers => headers, :body => rdf_data)
|
64
|
+
|
65
|
+
return res.code == 201
|
66
|
+
end
|
67
|
+
|
68
|
+
def sparql_pull(query)
|
69
|
+
path = "/DAV/home/#{@username}/rdf_sink" # XXX: shouldn't this be /sparql?
|
70
|
+
res = http_request("POST", path, query, {
|
71
|
+
"Content-Type" => "application/sparql-query"
|
72
|
+
})
|
73
|
+
return res.code == "200" # XXX: always returns 409
|
74
|
+
end
|
75
|
+
|
76
|
+
# query is a string or an array of strings
|
77
|
+
def sparql_query(query)
|
78
|
+
query = query.join("\n\n") + "\n" rescue query
|
79
|
+
|
80
|
+
path = "/DAV/home/#{@username}/query"
|
81
|
+
|
82
|
+
auth = Base64.encode64([@username, @password].join(":")).strip
|
83
|
+
headers = {
|
84
|
+
"Authorization" => "Basic #{auth}", # XXX: seems like this should be built into Typhoeus!?
|
85
|
+
"Content-Type" => "application/sparql-query"
|
86
|
+
}
|
87
|
+
res = Typhoeus::Request.put("#{@host}:#{@port}#{path}",
|
88
|
+
:headers => headers, :body => query)
|
89
|
+
|
90
|
+
return res.code == 201
|
91
|
+
end
|
92
|
+
|
93
|
+
def http_request(method, path, body, headers={}) # TODO: switch to Typhoeus
|
94
|
+
uri = URI.parse("#{@host}:#{@port}#{path}")
|
95
|
+
|
96
|
+
req = Net::HTTP.const_get(method.downcase.capitalize).new(uri.to_s)
|
97
|
+
req.basic_auth(@username, @password)
|
98
|
+
headers.each { |key, value| req[key] = value }
|
99
|
+
req.body = body
|
100
|
+
|
101
|
+
return Net::HTTP.new(uri.host, uri.port).request(req)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require "minitest/autorun"
|
3
|
+
require 'webmock/test_unit'
|
4
|
+
|
5
|
+
require "iq_triplestorage/virtuoso_adaptor"
|
6
|
+
|
7
|
+
class VirtuosoTest < MiniTest::Unit::TestCase
|
8
|
+
|
9
|
+
def setup
|
10
|
+
# HTTP request mocking
|
11
|
+
@observers = [] # one per request
|
12
|
+
WebMock.stub_request(:any, /.*example.org.*/).with do |req|
|
13
|
+
# not using WebMock's custom assertions as those didn't seem to provide
|
14
|
+
# sufficient flexibility
|
15
|
+
fn = @observers.shift
|
16
|
+
raise(TypeError, "missing request observer") unless fn
|
17
|
+
fn.call(req)
|
18
|
+
true
|
19
|
+
end.to_return do |req|
|
20
|
+
{ :status => req.uri.to_s.end_with?("/rdf_sink") ? 200 : 201 }
|
21
|
+
end
|
22
|
+
|
23
|
+
@username = "foo"
|
24
|
+
@password = "bar"
|
25
|
+
@host = "example.org"
|
26
|
+
@port = 80
|
27
|
+
@adaptor = IqTriplestorage::VirtuosoAdaptor.new("http://#{@host}", @port,
|
28
|
+
@username, @password)
|
29
|
+
end
|
30
|
+
|
31
|
+
def teardown
|
32
|
+
WebMock.reset!
|
33
|
+
raise(TypeError, "unhandled request observer") unless @observers.length == 0
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_reset
|
37
|
+
uri = "http://example.com/foo"
|
38
|
+
|
39
|
+
@observers << lambda do |req|
|
40
|
+
ensure_basics(req)
|
41
|
+
assert_equal :post, req.method
|
42
|
+
assert_equal "/DAV/home/#{@username}/rdf_sink", req.uri.path
|
43
|
+
assert_equal "application/sparql-query", req.headers["Content-Type"]
|
44
|
+
assert_equal "CLEAR GRAPH <#{uri}>", req.body
|
45
|
+
end
|
46
|
+
assert @adaptor.reset(uri)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_pull
|
50
|
+
uri = "http://example.com/bar"
|
51
|
+
|
52
|
+
@observers << lambda do |req|
|
53
|
+
assert_equal "CLEAR GRAPH <#{uri}>", req.body
|
54
|
+
end
|
55
|
+
@observers << lambda do |req|
|
56
|
+
assert_equal :post, req.method
|
57
|
+
assert_equal "/DAV/home/#{@username}/rdf_sink", req.uri.path
|
58
|
+
assert_equal "application/sparql-query", req.headers["Content-Type"]
|
59
|
+
assert_equal %(LOAD "#{uri}" INTO GRAPH <#{uri}>), req.body
|
60
|
+
end
|
61
|
+
assert @adaptor.update(uri)
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_push
|
65
|
+
uri = "http://example.com/baz"
|
66
|
+
|
67
|
+
rdf_data = <<-EOS.strip
|
68
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
69
|
+
<rdf:RDF xmlns="http://try.iqvoc.net/"
|
70
|
+
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
71
|
+
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
|
72
|
+
xmlns:owl="http://www.w3.org/2002/07/owl#"
|
73
|
+
xmlns:skos="http://www.w3.org/2004/02/skos/core#"
|
74
|
+
xmlns:dct="http://purl.org/dc/terms/"
|
75
|
+
xmlns:coll="http://try.iqvoc.net/collections/"
|
76
|
+
xmlns:schema="http://try.iqvoc.net/schema#">
|
77
|
+
<rdf:Description rdf:about="http://try.iqvoc.net/model_building">
|
78
|
+
<rdf:type rdf:resource="http://www.w3.org/2004/02/skos/core#Concept"/>
|
79
|
+
<skos:prefLabel xml:lang="en">Model building</skos:prefLabel>
|
80
|
+
<skos:narrower rdf:resource="http://try.iqvoc.net/model_rocketry"/>
|
81
|
+
<skos:narrower rdf:resource="http://try.iqvoc.net/radio-controlled_modeling"/>
|
82
|
+
<skos:narrower rdf:resource="http://try.iqvoc.net/scale_modeling"/>
|
83
|
+
<skos:broader rdf:resource="http://try.iqvoc.net/achievement_hobbies"/>
|
84
|
+
</rdf:Description>
|
85
|
+
</rdf:RDF>
|
86
|
+
EOS
|
87
|
+
|
88
|
+
@observers << lambda do |req|
|
89
|
+
assert_equal "CLEAR GRAPH <#{uri}>", req.body
|
90
|
+
end
|
91
|
+
@observers << lambda do |req|
|
92
|
+
assert_equal :put, req.method
|
93
|
+
assert req.uri.path.start_with?("/DAV/home/#{@username}/rdf_sink/")
|
94
|
+
assert_equal "application/rdf+xml", req.headers["Content-Type"]
|
95
|
+
assert_equal rdf_data, req.body
|
96
|
+
end
|
97
|
+
assert @adaptor.update(uri, rdf_data, "application/rdf+xml")
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_batch
|
101
|
+
data = {
|
102
|
+
"http://example.com/foo" => "<aaa> <bbb> <ccc> .\n<ddd> <eee> <fff> .",
|
103
|
+
"http://example.com/bar" => "<ggg> <hhh> <iii> .\n<jjj> <kkk> <lll> ."
|
104
|
+
}
|
105
|
+
|
106
|
+
@observers << lambda do |req|
|
107
|
+
data.keys.each do |graph_uri|
|
108
|
+
assert req.body.include?("CLEAR GRAPH <#{graph_uri}>")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
@observers << lambda do |req|
|
112
|
+
data.each do |graph_uri, ntriples|
|
113
|
+
assert req.body.
|
114
|
+
include?(<<-EOS)
|
115
|
+
INSERT IN GRAPH <#{graph_uri}> {
|
116
|
+
#{ntriples}
|
117
|
+
}
|
118
|
+
EOS
|
119
|
+
end
|
120
|
+
end
|
121
|
+
assert @adaptor.batch_update(data)
|
122
|
+
end
|
123
|
+
|
124
|
+
def ensure_basics(req) # TODO: rename
|
125
|
+
assert_equal "#{@host}:#{@port}", "#{req.uri.hostname}:#{req.uri.port}"
|
126
|
+
|
127
|
+
if auth_header = req.headers["Authorization"]
|
128
|
+
auth = Base64.encode64([@username, @password].join(":")).strip
|
129
|
+
assert_equal auth, auth_header
|
130
|
+
else
|
131
|
+
# MockWeb appears to prevent the Authorization header being set, instead
|
132
|
+
# retaining username and password in URI
|
133
|
+
assert req.uri.to_s.
|
134
|
+
start_with?("#{req.uri.scheme}://#{@username}:#{@password}@")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: iq_triplestorage
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- FND
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-23 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email:
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- .travis.yml
|
21
|
+
- Gemfile
|
22
|
+
- README.md
|
23
|
+
- Rakefile
|
24
|
+
- iq_triplestorage.gemspec
|
25
|
+
- lib/iq_triplestorage.rb
|
26
|
+
- lib/iq_triplestorage/virtuoso_adaptor.rb
|
27
|
+
- test/virtuoso_test.rb
|
28
|
+
homepage: http://github.com/innoq/iq_triplestorage
|
29
|
+
licenses: []
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
requirements: []
|
47
|
+
rubyforge_project: iq_triplestorage
|
48
|
+
rubygems_version: 1.8.23
|
49
|
+
signing_key:
|
50
|
+
specification_version: 3
|
51
|
+
summary: IqTriplestorage - library for interacting with RDF triplestores / quadstores
|
52
|
+
test_files:
|
53
|
+
- test/virtuoso_test.rb
|