rdf-virtuoso 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +73 -0
- data/lib/active_rdf.rb +14 -0
- data/lib/active_rdf/association_reflection.rb +26 -0
- data/lib/active_rdf/errors.rb +9 -0
- data/lib/active_rdf/exceptions.rb +83 -0
- data/lib/active_rdf/model.rb +69 -0
- data/lib/active_rdf/persistence.rb +185 -0
- data/lib/active_rdf/reflections.rb +19 -0
- data/lib/active_rdf/version.rb +10 -0
- data/lib/rdf/virtuoso.rb +8 -0
- data/lib/rdf/virtuoso/parser.rb +42 -0
- data/lib/rdf/virtuoso/prefixes.rb +54 -0
- data/lib/rdf/virtuoso/query.rb +632 -0
- data/lib/rdf/virtuoso/repository.rb +104 -0
- data/lib/rdf/virtuoso/version.rb +5 -0
- data/spec/active_rdf/persistence_spec.rb +31 -0
- data/spec/prefixes_spec.rb +48 -0
- data/spec/query_spec.rb +278 -0
- data/spec/repository_spec.rb +21 -0
- data/spec/spec_helper.rb +12 -0
- metadata +243 -0
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# rdf-virtuoso: Ruby Virtuoso adapter for RDF.rb
|
2
|
+
The intent of this class is to act as an abstraction for clients wishing to connect and manipulate linked data stored in a Virtuoso Quad store.
|
3
|
+
|
4
|
+
## How?
|
5
|
+
RDF::Virtuoso::Repository builds on RDF.rb and is the main connection class built on top of APISmith to establish the read and write methods to a Virtuoso store SPARQL endpoint.
|
6
|
+
RDF::Virtuoso::Query extends RDF::Query and adds SPARQL 1.1. update methods (insert, delete, aggregates, etc.).
|
7
|
+
|
8
|
+
For examples on use, see ./spec/client_spec.rb and ./spec/query_spec.rb
|
9
|
+
|
10
|
+
### A simple example
|
11
|
+
|
12
|
+
This example assumes you have a local installation of Virtoso running at standard port 8890
|
13
|
+
|
14
|
+
#### Setup Repository connection connection with auth
|
15
|
+
|
16
|
+
uri = "http://localhost:8890"
|
17
|
+
REPO = RDF::Virtuoso::Repository.new(uri, :username => 'admin', :password => 'secret', :auth_method => 'digest')
|
18
|
+
|
19
|
+
:auth_method can be 'digest' or 'basic'. a repository connection without auth requires only uri
|
20
|
+
|
21
|
+
#### INSERT WHERE query example
|
22
|
+
|
23
|
+
QUERY = RDF::Virtuoso::Query
|
24
|
+
graph = RDF::URI.new("http://test.com")
|
25
|
+
subject = RDF::URI.new("http://subject")
|
26
|
+
|
27
|
+
query = QUERY.insert([subject, :p, "object"]).graph(graph).where([subject, :p, :o])
|
28
|
+
result = REPO.insert(query)
|
29
|
+
|
30
|
+
#### A count query example
|
31
|
+
|
32
|
+
QUERY = RDF::Virtuoso::Query
|
33
|
+
graph = RDF::URI.new("http://test.com")
|
34
|
+
type = RDF::BIBO.Document
|
35
|
+
|
36
|
+
count = REPO.select.where(:s, type, :o).count(:s).graph(graph)
|
37
|
+
|
38
|
+
## Rails specifics
|
39
|
+
Working on a prototype Rails application for negotiating and manipulating linked data in an RDF store, I discovered the lack of a reasonably current library to bridge the gap between the fairly well-established, modular RDF.rb library and a Rails 3 application. I wanted to be able to manipulate RDF data in a convenient, ActiveRecord/ActiveModel way. It turned out to be fairly non-trivial to mimic true AR/AM behavior and this is more or less the groundwork and result of my experimentation. I now have a much better idea of how to proceed, I just need the time to really go deep into this.
|
40
|
+
An example prototype that exercises this library can be found here: https://github.com/digibib/booky
|
41
|
+
|
42
|
+
It must be stressed that this is still early days, with lots of refactoring and abstraction to be done, along with very specific functionality targeted at the prototype I've been working on. So anyone wanting a more generalized approach would be well served by waiting until I'm further along.
|
43
|
+
|
44
|
+
Essentially, a model in a Rails 3 app subclasses ActiveRDF::Model, which in itself includes the following modules:
|
45
|
+
ActiveAttr::Model - https://github.com/cgriego/active_attr
|
46
|
+
ActiveModel::Dirty - http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
|
47
|
+
ActiveRDF::Persistence - responsible for handling persistence, in this gem
|
48
|
+
|
49
|
+
In a nutshell:
|
50
|
+
|
51
|
+
ActiveRDF::Model provides common functionality for models along with some mixed-in 3rd-party modules
|
52
|
+
|
53
|
+
ActiveRDF::Persistence provides some of the functionality a Rails model may expect, along with some placeholder methods that are waiting for AR/Arel type implementations. All communication is done via the query language SPARQL, (version 1.1 as of this writing).
|
54
|
+
|
55
|
+
RDF::Virtuoso::Client provides a rudimentary connection class for interacting with a Virtuoso server. It is inspired by API Smith (https://github.com/filtersquad/api_smith) which has some nice features, such as a convenient way to specify a Parser class for a given mimetype. See RDF::Virtuoso::Parser for example, and the method RDF::Virtuoso::Client#api_get.
|
56
|
+
|
57
|
+
## Challenges
|
58
|
+
In no particular order:
|
59
|
+
|
60
|
+
* In RDF data, the equivalent of a primary key in a relational database is called the subject (part of each of a set of triples), which is represented by a URI which looks like an absolute URL. When Rails does its magic with routes and friends, it assumes by default that it's dealing with an Integer, which is fine for a relational database id column which is auto-generated by the RDBM, unique and very often an int. Under the covers, to_i is called on id. This does not work for URLs, obviously. Also, you can't just send unencoded URLs over the wire as part of a path in a route. Anyway, this whole issue needed to be solved quickly, so for now RDF subjects that are part of a path are encoded and then decoded before insertion.
|
61
|
+
* On the topic of unique ids, relational databases can be told to autogenerate unique primary keys on insert for you. Here, we have to use a library (UUID) to generate unique ids before insert.
|
62
|
+
* On the whole, when dealing with RDF subjects, care has to be taken how they are constructed. They can end with either a forward-slash or a hash, followed by some unique identifier. Seems like the hash-notation is preferred if the data is ever to be be exported as turtle files (suffix ttl) and the unique identifier potentially starts with an integer. Whether that is a bug in the writer implementation is unclear, but better safe than sorry.
|
63
|
+
* An update in an RDF store is a so-called modify and consists of one or more delete statements followed by one or more insert statements. Meaning that when a triple has changed, the original has to be deleted before the modified triple is inserted. There is a potential for leaving the data in an inconsistent state if, for instance, the delete directive isn't constructed correctly and the insert is. Or vice versa. With ActiveRecord, database writes are automatically wrapped in transactions that will roll back the data on failure. This too needed to be implemented explicitly, as seen in ActiveRDF::Persistence#update_attributes, using the SimpleTransaction libray.
|
64
|
+
|
65
|
+
## Notes
|
66
|
+
The following classes are not yet in use or will probably disapper:
|
67
|
+
|
68
|
+
* ActiveRDF::AssociationReflection
|
69
|
+
* ActiveRDF::Exceptions
|
70
|
+
* ActiveRDF::Reflections
|
71
|
+
|
72
|
+
|
73
|
+
|
data/lib/active_rdf.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_model'
|
3
|
+
require 'active_attr'
|
4
|
+
require 'active_rdf/exceptions'
|
5
|
+
require 'active_rdf/errors'
|
6
|
+
require 'active_rdf/version'
|
7
|
+
|
8
|
+
module ActiveRDF
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
|
11
|
+
autoload :Model
|
12
|
+
autoload :Persistence
|
13
|
+
autoload :Reflection
|
14
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class AssociationReflection
|
2
|
+
attr_reader :macro
|
3
|
+
attr_reader :name
|
4
|
+
attr_reader :options
|
5
|
+
|
6
|
+
def initialize(macro, name, options = {})
|
7
|
+
@macro = macro
|
8
|
+
@name = name
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def class_name
|
13
|
+
@class_name ||= (options[:type] || derive_class_name).to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def klass
|
17
|
+
@klass ||= class_name.constantize
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def derive_class_name
|
23
|
+
name.to_s.camelize
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module ActiveRDF
|
2
|
+
|
3
|
+
class ConnectionError < StandardError # :nodoc:
|
4
|
+
attr_reader :response
|
5
|
+
|
6
|
+
def initialize(response, message = nil)
|
7
|
+
@response = response
|
8
|
+
@message = message
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
message = "Failed."
|
13
|
+
message << " Response code = #{response.code}." if response.respond_to?(:code)
|
14
|
+
message << " Response message = #{response.message}." if response.respond_to?(:message)
|
15
|
+
message
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Raised when a Timeout::Error occurs.
|
20
|
+
class TimeoutError < ConnectionError
|
21
|
+
def initialize(message)
|
22
|
+
@message = message
|
23
|
+
end
|
24
|
+
def to_s; @message ;end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Raised when a OpenSSL::SSL::SSLError occurs.
|
28
|
+
class SSLError < ConnectionError
|
29
|
+
def initialize(message)
|
30
|
+
@message = message
|
31
|
+
end
|
32
|
+
def to_s; @message ;end
|
33
|
+
end
|
34
|
+
|
35
|
+
# 3xx Redirection
|
36
|
+
class Redirection < ConnectionError # :nodoc:
|
37
|
+
def to_s
|
38
|
+
response['Location'] ? "#{super} => #{response['Location']}" : super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class MissingPrefixParam < ArgumentError # :nodoc:
|
43
|
+
end
|
44
|
+
|
45
|
+
# 4xx Client Error
|
46
|
+
class ClientError < ConnectionError # :nodoc:
|
47
|
+
end
|
48
|
+
|
49
|
+
# 400 Bad Request
|
50
|
+
class BadRequest < ClientError # :nodoc:
|
51
|
+
end
|
52
|
+
|
53
|
+
# 401 Unauthorized
|
54
|
+
class UnauthorizedAccess < ClientError # :nodoc:
|
55
|
+
end
|
56
|
+
|
57
|
+
# 403 Forbidden
|
58
|
+
class ForbiddenAccess < ClientError # :nodoc:
|
59
|
+
end
|
60
|
+
|
61
|
+
# 404 Not Found
|
62
|
+
class ResourceNotFound < ClientError # :nodoc:
|
63
|
+
end
|
64
|
+
|
65
|
+
# 409 Conflict
|
66
|
+
class ResourceConflict < ClientError # :nodoc:
|
67
|
+
end
|
68
|
+
|
69
|
+
# 410 Gone
|
70
|
+
class ResourceGone < ClientError # :nodoc:
|
71
|
+
end
|
72
|
+
|
73
|
+
# 5xx Server Error
|
74
|
+
class ServerError < ConnectionError # :nodoc:
|
75
|
+
end
|
76
|
+
|
77
|
+
# 405 Method Not Allowed
|
78
|
+
class MethodNotAllowed < ClientError # :nodoc:
|
79
|
+
def allowed_methods
|
80
|
+
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'uuid'
|
2
|
+
require 'transaction/simple'
|
3
|
+
require 'rdf'
|
4
|
+
require 'active_rdf/reflections'
|
5
|
+
|
6
|
+
module ActiveRDF
|
7
|
+
|
8
|
+
class Model
|
9
|
+
include ActiveAttr::Model
|
10
|
+
include ActiveModel::Dirty
|
11
|
+
include ActiveRDF::Persistence
|
12
|
+
|
13
|
+
# All children should have these attributes
|
14
|
+
attribute :id, type: String
|
15
|
+
attribute :subject, type: String
|
16
|
+
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :reflections
|
20
|
+
|
21
|
+
def graph
|
22
|
+
url = RDF::URI.new("http://data.deichman.no")
|
23
|
+
if defined?(Rails)
|
24
|
+
url = url.join Rails.env unless (Rails.env.production? || Rails.env.staging?)
|
25
|
+
end
|
26
|
+
url / self.name.downcase.pluralize
|
27
|
+
end
|
28
|
+
|
29
|
+
def encode(string)
|
30
|
+
[string].pack('m0')
|
31
|
+
end
|
32
|
+
|
33
|
+
def decode(string)
|
34
|
+
string.unpack('m')[0]
|
35
|
+
end
|
36
|
+
|
37
|
+
def from_param(param)
|
38
|
+
decode param
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def inherited(child)
|
44
|
+
child.instance_variable_set :@reflections, @reflections.dup
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end # Class methods
|
48
|
+
|
49
|
+
def type
|
50
|
+
self.class.type
|
51
|
+
end
|
52
|
+
|
53
|
+
# When using an object's subject, which comes in the format http://example.org/object#123 as
|
54
|
+
# a query param, we must encode it first
|
55
|
+
def to_param
|
56
|
+
self.class.encode self.subject
|
57
|
+
end
|
58
|
+
|
59
|
+
def graph
|
60
|
+
self.class.graph
|
61
|
+
end
|
62
|
+
|
63
|
+
extend Reflections
|
64
|
+
|
65
|
+
@reflections = HashWithIndifferentAccess.new
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module ActiveRDF
|
2
|
+
module Persistence
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
|
7
|
+
# Override ActiveAttr::Attributes.attribute=(name, value)
|
8
|
+
def attribute=(name, value)
|
9
|
+
@attributes ||= {}
|
10
|
+
# We'll assume that nil and "" are equivalent
|
11
|
+
unless (@attributes[name].blank? && value.blank?) || (@attributes[name] == value)
|
12
|
+
send("#{name}_will_change!")
|
13
|
+
end
|
14
|
+
@attributes[name] = value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
def connection
|
21
|
+
# TODO: make this behave like AM/AR Connection
|
22
|
+
CLIENT
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(attrs = nil)
|
26
|
+
object = new(attrs)
|
27
|
+
object.save
|
28
|
+
object
|
29
|
+
end
|
30
|
+
|
31
|
+
def create!(attrs = nil)
|
32
|
+
object = new(attrs)
|
33
|
+
object.save!
|
34
|
+
object
|
35
|
+
end
|
36
|
+
|
37
|
+
def before_create(method)
|
38
|
+
# Placeholder until implemented in ActiveRDF::Callbacks
|
39
|
+
end
|
40
|
+
|
41
|
+
def before_save(method)
|
42
|
+
# Placeholder until implemented in ActiveRDF::Callbacks
|
43
|
+
end
|
44
|
+
|
45
|
+
# @see: http://rdf.rubyforge.org/RDF/Query/Solutions.html
|
46
|
+
def order(variable)
|
47
|
+
end
|
48
|
+
|
49
|
+
def where(conditions)
|
50
|
+
end
|
51
|
+
|
52
|
+
def scope(variable, conditions)
|
53
|
+
end
|
54
|
+
|
55
|
+
def scoped
|
56
|
+
end
|
57
|
+
|
58
|
+
def count
|
59
|
+
query = "SELECT COUNT(DISTINCT ?s) WHERE { GRAPH <#{self.graph}> { ?s a <#{self.type}> }}"
|
60
|
+
result = CLIENT.select(query)
|
61
|
+
result.first['callret-0'].to_i
|
62
|
+
end
|
63
|
+
|
64
|
+
def find(object_or_id, conditions = {})
|
65
|
+
|
66
|
+
subject = case object_or_id
|
67
|
+
when String then decode(object_or_id)
|
68
|
+
when self then object_or_id.subject
|
69
|
+
else raise ActiveModel::MissingAttributeError.new(object_or_id.inspect)
|
70
|
+
end
|
71
|
+
|
72
|
+
find_by_subject(subject, conditions = {})
|
73
|
+
end
|
74
|
+
|
75
|
+
def first
|
76
|
+
all(limit: 1).first
|
77
|
+
end
|
78
|
+
|
79
|
+
def execute(sql)
|
80
|
+
results = []
|
81
|
+
solutions = CLIENT.select(sql)
|
82
|
+
solutions.each do |solution|
|
83
|
+
record = new
|
84
|
+
solution.each_binding do |name, value|
|
85
|
+
record[name] = value.to_s
|
86
|
+
end
|
87
|
+
if record.subject.present?
|
88
|
+
record.id = id_for(record.subject)
|
89
|
+
record.changed_attributes.clear
|
90
|
+
end
|
91
|
+
results << record
|
92
|
+
end
|
93
|
+
results
|
94
|
+
end
|
95
|
+
|
96
|
+
# TODO: set baseurl via config
|
97
|
+
def subject_for(id)
|
98
|
+
RDF::URI('http://data.deichman.no') / self.name.downcase / "#" / id
|
99
|
+
end
|
100
|
+
|
101
|
+
def id_for(subject)
|
102
|
+
subject.to_s.split("#").last
|
103
|
+
end
|
104
|
+
|
105
|
+
def destroy_all
|
106
|
+
query = "DELETE FROM <#{self.graph}> { ?s ?p ?o } WHERE { GRAPH <#{self.graph}> { ?s a <#{self.type}> . ?s ?p ?o } }"
|
107
|
+
connection.delete(query)
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
# Instance methods
|
113
|
+
|
114
|
+
def connection
|
115
|
+
self.class.connection
|
116
|
+
end
|
117
|
+
|
118
|
+
def save
|
119
|
+
return false unless self.valid?
|
120
|
+
create_or_update
|
121
|
+
end
|
122
|
+
|
123
|
+
def save!
|
124
|
+
unless self.valid?
|
125
|
+
raise ActiveRecord::RecordInvalid.new(self)
|
126
|
+
end
|
127
|
+
create_or_update
|
128
|
+
end
|
129
|
+
|
130
|
+
def destroy
|
131
|
+
subject = subject_for(self.id)
|
132
|
+
query =
|
133
|
+
<<-q
|
134
|
+
DELETE FROM <#{graph}> { <#{subject}> ?p ?o }
|
135
|
+
WHERE { <#{subject}> ?p ?o }
|
136
|
+
q
|
137
|
+
result = connection.delete(query)
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
def update_attributes(attributes)
|
142
|
+
self.extend(::Transaction::Simple)
|
143
|
+
status = false
|
144
|
+
begin
|
145
|
+
self.start_transaction
|
146
|
+
self.assign_attributes(attributes)
|
147
|
+
status = save
|
148
|
+
self.commit_transaction
|
149
|
+
rescue Exception
|
150
|
+
self.rewind_transaction
|
151
|
+
self.abort_transaction
|
152
|
+
end
|
153
|
+
status
|
154
|
+
end
|
155
|
+
|
156
|
+
def reload
|
157
|
+
self.attributes = self.class.find(self).attributes
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
def new_record?
|
162
|
+
self.id.nil?
|
163
|
+
end
|
164
|
+
|
165
|
+
def persisted?
|
166
|
+
!new_record?
|
167
|
+
end
|
168
|
+
|
169
|
+
def subject_for(id)
|
170
|
+
self.class.subject_for(id)
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def create_or_update
|
176
|
+
result = new_record? ? create : update
|
177
|
+
result != false
|
178
|
+
end
|
179
|
+
|
180
|
+
def guid
|
181
|
+
UUID.generate(:compact)
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|