activerdf 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.
@@ -0,0 +1,325 @@
1
+ # Represents an RDF resource and manages manipulations of that resource,
2
+ # including data lookup (e.g. eyal.age), data updates (e.g. eyal.age=20),
3
+ # class-level lookup (Person.find_by_name 'eyal'), and class-membership
4
+ # (eyal.class ...Person)
5
+ #
6
+ # Author:: Eyal Oren
7
+ # Copyright:: (c) 2005-2006
8
+ # License:: LGPL
9
+
10
+ require 'active_rdf'
11
+ require 'objectmanager/object_manager'
12
+ require 'objectmanager/namespace'
13
+ require 'queryengine/query'
14
+
15
+ # TODO: add unit test to validate class construction and queries on them
16
+ module RDFS
17
+ class RDFS::Resource
18
+ # adding accessor to the class uri:
19
+ # the uri of the rdf resource being represented by this class
20
+ class << self
21
+ attr_accessor :class_uri
22
+ end
23
+
24
+ # uri of the resource (for instances of this class: rdf resources)
25
+ attr_reader :uri
26
+
27
+ # creates new resource representing an RDF resource
28
+ def initialize uri
29
+ raise ActiveRdfError, "creating resource <#{uri}>" unless uri.is_a?(String)
30
+
31
+ # $log.debug "RDFS::Resource new: initializing new Resource with #{uri}"
32
+ @uri = uri
33
+ end
34
+
35
+ # setting our own class uri to rdfs:resource
36
+ # (has to be done after defining our RDFS::Resource.new
37
+ # because it cannot be found in Namespace.lookup otherwise)
38
+ self.class_uri = Namespace.lookup(:rdfs, :Resource)
39
+
40
+ ##### ######
41
+ ##### start of instance-level code
42
+ ##### ######
43
+
44
+ # a resource is same as another if they both represent the same uri
45
+ def ==(other)
46
+ if other.respond_to?(:uri)
47
+ other.uri == self.uri
48
+ else
49
+ false
50
+ end
51
+ end
52
+ alias_method 'eql?','=='
53
+
54
+ # overriding hash to use uri.hash
55
+ # needed for array.uniq
56
+ def hash
57
+ uri.hash
58
+ end
59
+
60
+ # overriding sort based on uri
61
+ def <=>(other)
62
+ uri <=> other.uri
63
+ end
64
+
65
+ ##### #####
66
+ ##### class level methods #####
67
+ ##### #####
68
+
69
+ # returns the predicates that have this resource as their domain (applicable
70
+ # predicates for this resource)
71
+ def Resource.predicates
72
+ domain = Namespace.lookup(:rdfs, :domain)
73
+ Query.new.distinct(:p).where(:p, domain, class_uri).execute || []
74
+ end
75
+
76
+ # manages invocations such as Person.find_by_name
77
+ def Resource.method_missing(method, *args)
78
+ method_name = method.to_s
79
+
80
+ $log.debug "RDFS::Resource: method_missing on class: called with method name #{method}"
81
+
82
+ # extract predicates on which to match
83
+ # e.g. find_by_name, find_by_name_and_age
84
+ if match = /find_by_(.+)/.match(method_name)
85
+ # find searched attributes, e.g. name, age
86
+ attributes = match[1].split('_and_')
87
+
88
+ # get list of possible predicates for this class
89
+ possible_predicates = predicates
90
+
91
+ # build query looking for all resources with the given parameters
92
+ query = Query.new.distinct(:s)
93
+
94
+ # add where clause for each attribute-value pair,
95
+ # looking into possible_predicates to figure out
96
+ # which full-URI to use for each given parameter (heuristic)
97
+
98
+ attributes.each_with_index do |atr,i|
99
+ possible_predicates.each do |pred|
100
+ query.where(:s, pred, args[i]) if Namespace.localname(pred) == atr
101
+ end
102
+ end
103
+
104
+ # execute query
105
+ $log.debug "RDFS::Resource: method_missing on class: executing query: #{query}"
106
+ return query.execute
107
+ end
108
+
109
+ # otherwise, if no match found, raise NoMethodError (in superclass)
110
+ $log.warn 'RDFS::Resource: method_missing on class: method not matching find_by_*'
111
+ super
112
+ end
113
+
114
+ # returns array of all instances of this class (e.g. Person.find_all)
115
+ # (always returns collection)
116
+ def Resource.find_all
117
+ query = Query.new.distinct(:s).where(:s, Namespace.lookup(:rdf,:type), class_uri)
118
+ if block_given?
119
+ query.execute do |resource|
120
+ yield resource
121
+ end
122
+ else
123
+ query.execute(:flatten => false)
124
+ end
125
+ end
126
+
127
+ ##### #####
128
+ ##### instance level methods #####
129
+ ##### #####
130
+
131
+ # manages invocations such as eyal.age
132
+ def method_missing(method, *args)
133
+ # possibilities:
134
+ # 1. eyal.age is a property of eyal (triple exists <eyal> <age> "30")
135
+ # evidence: eyal age ?a, ?a is not nil (only if value exists)
136
+ # action: return ?a
137
+ #
138
+ # 2. eyal's class is in domain of age, but does not have value for eyal
139
+ # explain: eyal is a person and some other person (not eyal) has an age
140
+ # evidence: eyal type ?c, age domain ?c
141
+ # action: return nil
142
+ #
143
+ # 3. eyal.age = 30 (setting a value for a property)
144
+ # explain: eyal has (or could have) a value for age, and we update that value
145
+ # complication: we need to find the full URI for age (by looking at
146
+ # possible predicates to use
147
+ # evidence: eyal age ?o (eyal has a value for age now, we're updating it)
148
+ # evidence: eyal type ?c, age domain ?c (eyal could have a value for age, we're setting it)
149
+ # action: add triple (eyal, age, 30), return 30
150
+ #
151
+ # 4. eyal.age is a custom-written method in class Person
152
+ # evidence: eyal type ?c, ?c.methods includes age
153
+ # action: inject age into eyal and invoke
154
+
155
+ # maybe change order in which to check these, checking (4) is probably
156
+ # cheaper than (1)-(2) but (1) and (2) are probably more probable (getting
157
+ # attribute values over executing custom methods)
158
+
159
+ $log.debug "RDFS::Resource: method_missing on instance: called with method name #{method}"
160
+
161
+ # are we doing an update or not?
162
+ # checking if method ends with '='
163
+
164
+ if method.to_s[-1..-1] == '='
165
+ methodname = method.to_s[0..-2]
166
+ update = true
167
+ else
168
+ methodname = method.to_s
169
+ update = false
170
+ end
171
+
172
+ candidates = if update
173
+ class_level_predicates
174
+ else
175
+ direct_predicates
176
+ end
177
+
178
+ # checking possibility (1) and (3)
179
+ candidates.each do |pred|
180
+ if Namespace.localname(pred) == methodname
181
+ # found a property invocation of eyal: option 1) or 2)
182
+ # query execution will return either the value for the predicate (1)
183
+ # or nil (2)
184
+ if update
185
+ # TODO: delete old value if overwriting
186
+ # FederiationManager.delete(self, pred, nil)
187
+
188
+ # handling eyal.friends = [armin, andreas] --> expand array values
189
+ args.each do |value|
190
+ FederationManager.add(self, pred, value)
191
+ end
192
+ return args
193
+ else
194
+ # look into args, if it contains a hash with {:array => true} then
195
+ # we should not flatten the query results
196
+ return get_property_value(pred, args)
197
+ end
198
+ end
199
+ end
200
+
201
+ raise ActiveRdfError, "could not set #{methodname} to #{args}: no suitable predicate found. Maybe you are missing some shcema information?" if update
202
+
203
+ # get/set attribute value did not succeed, so checking option (2) and (4)
204
+
205
+ # checking possibility (2), it is not handled correctly above since we use
206
+ # direct_predicates instead of class_level_predicates. If we didn't find
207
+ # anything with direct_predicates, we need to try the
208
+ # class_level_predicates. Only if we don't find either, we
209
+ # throw "method_missing"
210
+ candidates = class_level_predicates
211
+
212
+ # if any of the class_level candidates fits the sought method, then we
213
+ # found situation (2), so we return nil or [] depending on the {:array =>
214
+ # true} value
215
+ if candidates.any?{|c| Namespace.localname(c) == methodname}
216
+ return_ary = args[0][:array] if args[0].is_a? Hash
217
+ if return_ary
218
+ return []
219
+ else
220
+ return nil
221
+
222
+ end
223
+ end
224
+
225
+ # checking possibility (4)
226
+ # TODO: implement search strategy to select in which class to invoke
227
+ # e.g. if to_s defined in Resource and in Person we should use Person
228
+ $log.debug "RDFS::Resource: method_missing on instance: branch selected: execution of custom class method"
229
+ self.class.each do |klass|
230
+ if klass.instance_methods.include?(method.to_s)
231
+ _dup = klass.new(uri)
232
+ return _dup.send(method,*args)
233
+ end
234
+ end
235
+
236
+ # if none of the three possibilities work out, we don't know this method
237
+ # invocation, but we don't want to throw NoMethodError, instead we return
238
+ # nil, so that eyal.age does not raise error, but returns nil. (in RDFS,
239
+ # we are never sure that eyal cannot have an age, we just dont know the
240
+ # age right now)
241
+ nil
242
+ end
243
+
244
+ # returns classes to which this resource belongs (according to rdf:type)
245
+ def class
246
+ types.collect do |type|
247
+ ObjectManager.construct_class(type)
248
+ end
249
+ end
250
+
251
+ def type
252
+ get_property_value(Namespace.lookup(:rdf,:type))
253
+ end
254
+
255
+ # overrides built-in instance_of? to use rdf:type definitions
256
+ def instance_of?(klass)
257
+ self.class.include?(klass)
258
+ end
259
+
260
+ # returns all predicates that fall into the domain of the rdf:type of this
261
+ # resource
262
+ def class_level_predicates
263
+ type = Namespace.lookup(:rdf, 'type')
264
+ domain = Namespace.lookup(:rdfs, 'domain')
265
+ Query.new.distinct(:p).where(self,type,:t).where(:p, domain, :t).execute || []
266
+ end
267
+
268
+ # returns all predicates that are directly defined for this resource
269
+ def direct_predicates(distinct = true)
270
+ if distinct
271
+ q = Query.new.distinct(:p)
272
+ else
273
+ q = Query.new.select(:p)
274
+ end
275
+ q.where(self,:p, :o).execute(:flatten => false) || []
276
+ end
277
+
278
+ def property_accessors
279
+ direct_predicates.collect {|pred| Namespace.localname(pred) }
280
+ end
281
+
282
+ # returns all rdf:types of this resource
283
+ def types
284
+ type = Namespace.lookup(:rdf, :type)
285
+
286
+ # we lookup the type in the database
287
+ types = Query.new.distinct(:t).where(self,type,:t).execute(:flatten => false)
288
+
289
+ # if we dont know it, we return Resource (as toplevel)
290
+ # this should in theory actually never happen (since any node is a rdfs:Resource)
291
+ # but could happen if the subject is unknown to the database
292
+ # or if the database does not support RDFS inferencing
293
+ return [Namespace.lookup(:rdfs,"Resource")] if types.empty?
294
+ return types
295
+ end
296
+
297
+ # alias include? to ==, so that you can do paper.creator.include?(eyal)
298
+ # without worrying whether paper.creator is single- or multi-valued
299
+ alias include? ==
300
+
301
+ # returns uri of resource, can be overridden in subclasses
302
+ def to_s
303
+ "resource: #{uri}"
304
+ end
305
+
306
+ def label(*args)
307
+ label = get_property_value(Namespace.lookup(:rdfs,:label)) || Namespace.localname(self)
308
+
309
+ # empty labels are not useful: replace them by localname
310
+ label = Namespace.localname(self) if label.empty?
311
+
312
+ # if we have no localname, use full uri
313
+ label = uri if label.empty?
314
+
315
+ label
316
+ end
317
+
318
+ private
319
+ def get_property_value(predicate, args=[])
320
+ return_ary = args[0][:array] if args[0].is_a?(Hash)
321
+ flatten_results = !return_ary
322
+ Query.new.distinct(:o).where(self, predicate, :o).execute(:flatten => flatten_results)
323
+ end
324
+ end
325
+ end
@@ -0,0 +1,166 @@
1
+ # Represents a query on a datasource, abstract representation of SPARQL features
2
+ # is passed to federation/adapter for execution on data
3
+ #
4
+ # Author:: Eyal Oren
5
+ # Copyright:: (c) 2005-2006
6
+ # License:: LGPL
7
+ require 'active_rdf'
8
+ require 'federation/federation_manager'
9
+
10
+ # TODO add log for every method which constitues to the query
11
+
12
+ class Query
13
+ attr_reader :select_clauses, :where_clauses, :keywords, :limits, :offsets
14
+ bool_accessor :distinct, :ask, :select, :count, :keyword
15
+
16
+ def initialize
17
+ $log.debug "Query: initializing"
18
+ distinct = false
19
+ limit = nil
20
+ offset = nil
21
+ @select_clauses = []
22
+ @where_clauses = []
23
+ @keywords = {}
24
+ end
25
+
26
+ def clear_select
27
+ $log.debug "Query: clearing select query"
28
+ @select_clauses = []
29
+ distinct = false
30
+ end
31
+
32
+ def select *s
33
+ @select = true
34
+ s.each do |e|
35
+ @select_clauses << parametrise(e)
36
+ end
37
+ # removing duplicate select clauses
38
+ @select_clauses.uniq!
39
+ $log.debug "Query: the current select clauses are: #{@select_clauses.join(', ')}"
40
+ self
41
+ end
42
+
43
+ def ask
44
+ @ask = true
45
+ $log.debug "Query: the current select clauses are: #{@select_clauses.join(', ')}"
46
+ self
47
+ end
48
+
49
+ def distinct *s
50
+ @distinct = true
51
+ select(*s)
52
+ end
53
+ alias_method :select_distinct, :distinct
54
+
55
+ def count *s
56
+ @count = true
57
+ select(*s)
58
+ end
59
+
60
+ def limit(i)
61
+ @limits = i
62
+ self
63
+ end
64
+
65
+ def offset(i)
66
+ @offsets = i
67
+ self
68
+ end
69
+
70
+ def where s,p,o
71
+ case p
72
+ when :keyword
73
+ # treat keywords in where-clauses specially
74
+ keyword_where(s,o)
75
+ else
76
+ # remove duplicate variable bindings, e.g.
77
+ # where(:s,type,:o).where(:s,type,:oo) we should remove the second clause,
78
+ # since it doesn't add anything to the query and confuses the query
79
+ # generator.
80
+ # if you construct this query manually, you shouldn't! if your select
81
+ # variable happens to be in one of the removed clauses: tough luck.
82
+
83
+ unless s.respond_to?(:uri)
84
+ unless s.class == Symbol
85
+ $log.debug "Query: where: got a Subject which is no Symbol, and no RDFS::Resource, but is instead: #{s}"
86
+ raise(ActiveRdfError, "cannot add a where clause, in which s is not a resource and not a variable")
87
+ end
88
+ end
89
+ unless p.respond_to?(:uri)
90
+ unless p.class == Symbol
91
+ $log.debug "Query: where: got a Predicate which is no Symbol, and no RDFS::Resource, but is instead: #{p}"
92
+ raise(ActiveRdfError, "cannot add a where clause, in which s is not a resource and not a variable")
93
+ end
94
+ end
95
+
96
+ @where_clauses << [s,p,o].collect{|arg| parametrise(arg)}
97
+ end
98
+ self
99
+ end
100
+
101
+ # adds keyword constraint to the query. You can use all Ferret query syntax in
102
+ # the constraint (e.g. keyword_where(:s,'eyal|benjamin')
103
+ def keyword_where s,o
104
+ @keyword = true
105
+ s = parametrise(s)
106
+ if @keywords.include?(s)
107
+ @keywords[s] = @keywords[s] + ' ' + o
108
+ else
109
+ @keywords[s] = o
110
+ end
111
+ self
112
+ end
113
+
114
+ # this is not normal behaviour, the method is implemented inside FacetNavigation
115
+ # def replace_where_clause old,new
116
+ # return unless where_clauses.includes?(old)
117
+ # where_clauses.delete(old)
118
+ # where_clauses.insert(new)
119
+ # end
120
+
121
+ # execute query on data sources
122
+ # either returns result as array
123
+ # (flattened into single value unless specified otherwise)
124
+ # or executes a block (number of block variables should be
125
+ # same as number of select variables)
126
+ #
127
+ # usage: results = query.execute
128
+ # usage: query.execute do |s,p,o| ... end
129
+ def execute(options={:flatten => true}, &block)
130
+ $log.debug "Query: executing query: #{self.inspect}"
131
+
132
+ if block_given?
133
+ FederationManager.query(self) do |*clauses|
134
+ block.call(*clauses)
135
+ end
136
+ else
137
+ FederationManager.query(self, options)
138
+ end
139
+ end
140
+
141
+ def to_s
142
+ if ConnectionPool.read_adapters.empty?
143
+ inspect
144
+ else
145
+ ConnectionPool.read_adapters.first.translate(self)
146
+ end
147
+ end
148
+
149
+ def to_sp
150
+ require 'queryengine/query2sparql'
151
+ Query2SPARQL.translate(self)
152
+ end
153
+
154
+ private
155
+ def parametrise s
156
+ case s
157
+ when Symbol
158
+ s
159
+ #'?' + s.to_s
160
+ when RDFS::Resource
161
+ s
162
+ else
163
+ '"' + s.to_s + '"'
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,27 @@
1
+ # Translates abstract query into jars2 query
2
+ # ignores ASK queries
3
+ #
4
+ # Author:: Eyal Oren
5
+ # Copyright:: (c) 2005-2006
6
+ # License:: LGPL
7
+ require 'active_rdf'
8
+
9
+
10
+ class Query2Jars2
11
+ def self.translate(query)
12
+ str = ""
13
+ if query.select?
14
+ # concatenate each where clause using space: s p o
15
+ # and then concatenate the clauses using dot: s p o . s2 p2 o2 .
16
+ str << "#{query.where_clauses.collect{|w| w.collect{|w| '?'+w.to_s}.join(' ')}.join(" .\n")} ."
17
+ # TODO: should we maybe reverse the order on the where_clauses? it depends
18
+ # on Andreas' answer of the best order to give to jars2. Users would
19
+ # probably put the most specific stuff first, and join to get the
20
+ # interesting information. Maybe we should not touch it and let the user
21
+ # figure it out.
22
+ end
23
+
24
+ $log.debug "Query2Jars2: translated #{query} to #{str}"
25
+ return str
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ # translates abstract query into SPARQL that can be executed on SPARQL-compliant data source
2
+ #
3
+ # Author:: Eyal Oren
4
+ # Copyright:: (c) 2005-2006
5
+ # License:: LGPL
6
+ require 'active_rdf'
7
+
8
+
9
+ class Query2SPARQL
10
+ def self.translate(query)
11
+ str = ""
12
+ if query.select?
13
+ distinct = query.distinct? ? "DISTINCT " : ""
14
+ select_clauses = query.select_clauses.collect{|s| construct_clause(s)}
15
+
16
+ str << "SELECT #{distinct}#{select_clauses.join(' ')} "
17
+ str << "WHERE { #{where_clauses(query)} }"
18
+ elsif query.ask?
19
+ str << "ASK { #{where_clauses(query)} }"
20
+ end
21
+
22
+ $log.debug "Query2SPARQL: translated the query to #{str}"
23
+ return str
24
+ end
25
+
26
+ private
27
+ # concatenate each where clause using space (e.g. 's p o')
28
+ # and concatenate the clauses using dot, e.g. 's p o . s2 p2 o2 .'
29
+ def self.where_clauses(query)
30
+ where_clauses = query.where_clauses.collect do |triple|
31
+ triple.collect {|term| construct_clause(term)}.join(' ')
32
+ end
33
+ "#{where_clauses.join('. ')} ."
34
+ end
35
+
36
+ def self.construct_clause(term)
37
+ case term
38
+ when Symbol
39
+ '?' + term.to_s
40
+ when RDFS::Resource
41
+ '<' + term.uri + '>'
42
+ else
43
+ term.to_s
44
+ end
45
+ end
46
+
47
+ #declare the class level methods as private with these directives
48
+ private_class_method :where_clauses
49
+ private_class_method :construct_clause
50
+
51
+ end
data/lib/active_rdf.rb ADDED
@@ -0,0 +1,48 @@
1
+ # Loader of ActiveRDF library
2
+ #
3
+ # Author:: Eyal Oren and Renaud Delbru
4
+ # Copyright:: (c) 2005-2006 Eyal Oren and Renaud Delbru
5
+ # License:: LGPL
6
+
7
+ # adding active_rdf subdirectory to the ruby loadpath
8
+ file = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
9
+ this_dir = File.dirname(File.expand_path(file))
10
+ $: << this_dir
11
+ $: << this_dir + '/active_rdf/'
12
+
13
+ require 'active_rdf_helpers'
14
+ require 'active_rdf_log'
15
+
16
+ $log.info "ActiveRDF started, logging level: #{$log.level}"
17
+
18
+ # load standard classes that need to be loaded at startup
19
+ require 'objectmanager/resource'
20
+ require 'objectmanager/namespace'
21
+ require 'federation/connection_pool'
22
+ require 'queryengine/query'
23
+ require 'federation/active_rdf_adapter'
24
+
25
+ def load_adapter s
26
+ begin
27
+ require s
28
+ rescue StandardError => e
29
+ $log.info "could not load adapter #{s}: #{e}"
30
+ end
31
+ end
32
+
33
+ require 'rubygems'
34
+ #determine if we are installed as a gem right now:
35
+ if Gem::cache().search("activerdf").empty?
36
+ #we are not running as a gem
37
+ $log.info 'ActiveRDF is NOT installed as a Gem'
38
+ load_adapter this_dir + '/../activerdf-rdflite/lib/activerdf_rdflite/rdflite'
39
+ load_adapter this_dir + '/../activerdf-redland/lib/activerdf_redland/redland'
40
+ load_adapter this_dir + '/../activerdf-sparql/lib/activerdf_sparql/sparql'
41
+ load_adapter this_dir + '/../activerdf-yars/lib/activerdf_yars/jars2'
42
+ else
43
+ #we are indeed running as a gem
44
+ require 'gem_plugin'
45
+ $log.info 'ActiveRDF is installed as a Gem'
46
+ GemPlugin::Manager.instance.load "activerdf" => GemPlugin::INCLUDE
47
+ end
48
+
@@ -0,0 +1,12 @@
1
+ # defining ActiveRDF errors
2
+ class ActiveRdfError < StandardError
3
+ end
4
+
5
+ # adding bool_accessor to ruby
6
+ class Module
7
+ def bool_accessor *syms
8
+ attr_accessor(*syms)
9
+ syms.each { |sym| alias_method "#{sym}?", sym }
10
+ remove_method(*syms)
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ require 'logger'
2
+
3
+ $log =
4
+ begin
5
+ # us the rails logger if running under rails
6
+ RAILS_DEFAULT_LOGGER
7
+ rescue NameError
8
+ unless ENV['ACTIVE_RDF_LOG'].nil?
9
+ # write to environment variable $RDF_LOG if set
10
+ Logger.new(ENV['ACTIVE_RDF_LOG'], 1, 100*1024)
11
+ else
12
+ require 'tmpdir'
13
+ # else just write to the temp dir
14
+ Logger.new(Dir.tmpdir.to_s + "/activerdf.log", 1, 100*1024);
15
+ end
16
+ end
17
+
18
+ # if user has specified loglevel we use that, otherwise we use default level
19
+ # in the environment variable ACTIVE_RDF_LOG_LEVEL we expect numbers, which we
20
+ # have to convert
21
+ if ENV['ACTIVE_RDF_LOG_LEVEL'].nil?
22
+ $log.level = Logger::WARN
23
+ else
24
+ $log.level = ENV['ACTIVE_RDF_LOG_LEVEL'].to_i
25
+ end
26
+
27
+ class Logger
28
+ def debug_pp(message, variable)
29
+ if variable.respond_to?(:join)
30
+ if variable.empty?
31
+ debug(sprintf(message, "empty"))
32
+ else
33
+ debug(sprintf(message, variable.join(', ')))
34
+ end
35
+ else
36
+ if variable.nil?
37
+ debug(sprintf(message, 'empty'))
38
+ else
39
+ debug(sprintf(message, variable))
40
+ end
41
+ end
42
+ end
43
+ end