activerdf 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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