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 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,9 @@
1
+ module ActiveRDF
2
+
3
+ # Base class for all ActiveRDF errors
4
+ class ActiveRDFError < StandardError
5
+ end
6
+
7
+ class ResourceNotFoundError < ActiveRDFError
8
+ end
9
+ end
@@ -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