root/activerdf-1.6.10/lib/active_rdf/objectmanager/resource.rb @ 146

Revision 146, 20.9 KB (checked in by samuraraujo, 5 years ago)
Line 
1require 'active_rdf'
2require 'objectmanager/object_manager'
3require 'objectmanager/namespace'
4require 'queryengine/query'
5require 'instance_exec'
6
7module RDFS
8 
9 
10  # Represents an RDF resource and manages manipulations of that resource,
11  # including data lookup (e.g. eyal.age), data updates (e.g. eyal.age=20),
12  # class-level lookup (Person.find_by_name 'eyal'), and class-membership
13  # (eyal.class ...Person).
14 
15  class RDFS::Resource   
16    # adding accessor to the class uri:
17    # the uri of the rdf resource being represented by this class
18    class << self
19      attr_accessor :class_uri
20      def reset_cache()       
21        $triple = Hash.new
22        $all_predicates_array =nil
23        RDFS::Resource.find_all_predicates()
24      end
25    end
26   
27    # uri of the resource (for instances of this class: rdf resources)
28    attr_reader :uri
29    $triple
30    # creates new resource representing an RDF resource
31    def initialize uri
32      @uri = case uri
33        # allow Resource.new(other_resource)
34        when RDFS::Resource || BNode
35        uri.uri
36        # allow Resource.new('<uri>') by stripping out <>
37        when /^<([^>]*)>$/
38        $1
39        # allow Resource.new('uri')
40        when String
41        uri
42      else 
43        raise ActiveRdfError, "cannot create resource <#{uri}>"
44      end     
45      $triple = Hash.new if $triple == nil
46      @predicates = Hash.new
47    end
48   
49    # setting our own class uri to rdfs:resource
50    # (has to be done after defining our RDFS::Resource.new
51    # because it cannot be found in Namespace.lookup otherwise)
52    self.class_uri = Namespace.lookup(:rdfs, :Resource)
53   
54    def self.uri; class_uri.uri; end
55    def self.==(other)
56      other.respond_to?(:uri) ? other.uri == self.uri : false
57    end
58    def self.localname; Namespace.localname(self); end
59   
60    #####                        ######
61    ##### start of instance-level code
62    #####                        ######
63   
64    def abbreviation; [Namespace.prefix(uri).to_s, localname]; end
65    # a resource is same as another if they both represent the same uri
66    def ==(other);
67      other.respond_to?(:uri) ? other.uri == self.uri : false
68    end
69    alias_method 'eql?','=='
70   
71    # overriding hash to use uri.hash
72    # needed for array.uniq
73    def hash; uri.hash; end
74   
75    # overriding sort based on uri
76    def <=>(other); uri <=> other.uri; end
77    def to_ntriple; to_s;  end
78    def self.to_ntriple; "<#{class_uri.uri}>"; end
79   
80    def to_xml
81      base = Namespace.expand(Namespace.prefix(self),'').chop
82     
83      xml = "<?xml version=\"1.0\"?>\n"
84      xml += "<rdf:RDF xmlns=\"#{base}\#\"\n"
85      Namespace.abbreviations.each { |p| uri = Namespace.expand(p,''); xml += "  xmlns:#{p.to_s}=\"#{uri}\"\n" if uri != base + '#' }
86      xml += "  xml:base=\"#{base}\">\n"
87     
88      xml += "<rdf:Description rdf:about=\"\##{localname}\">\n"
89      direct_predicates.each do |p|
90        objects = Query.new.distinct(:o).where(self, p, :o).execute
91        objects.each do |obj|
92          prefix, localname = Namespace.prefix(p), Namespace.localname(p)
93          pred_xml = if prefix
94                       "%s:%s" % [prefix, localname]
95          else
96            p.uri
97          end
98         
99          case obj
100            when RDFS::Resource
101            xml += "  <#{pred_xml} rdf:resource=\"#{obj.uri}\"/>\n"
102            when LocalizedString
103            xml += "  <#{pred_xml} xml:lang=\"#{obj.lang}\">#{obj}</#{pred_xml}>\n"
104          else
105            xml += "  <#{pred_xml} rdf:datatype=\"#{obj.xsd_type.uri}\">#{obj}</#{pred_xml}>\n"
106          end
107        end
108      end
109      xml += "</rdf:Description>\n"
110      xml += "</rdf:RDF>"
111    end
112   
113    #####                       #####
114    ##### class level methods   #####
115    #####                       #####
116   
117    # returns the predicates that have this resource as their domain (applicable
118    # predicates for this resource)
119    def Resource.predicates
120      domain = Namespace.lookup(:rdfs, :domain)
121      Query.new.distinct(:p).where(:p, domain, class_uri).execute || []
122    end
123   
124    # manages invocations such as Person.find_by_name,
125    # Person.find_by_foaf::name, Person.find_by_foaf::name_and_foaf::knows, etc.
126    def Resource.method_missing(method, *args)
127      if /find_by_(.+)/.match(method.to_s)
128        $activerdflog.debug "constructing dynamic finder for #{method}"
129       
130        # construct proxy to handle delayed lookups
131        # (find_by_foaf::name_and_foaf::age)
132        proxy = DynamicFinderProxy.new($1, nil, *args)
133       
134        # if proxy already found a value (find_by_name) we will not get a more
135        # complex query, so return the value. Otherwise, return the proxy so that
136        # subsequent lookups are handled
137        return proxy.value || proxy
138      end
139    end
140   
141    # returns array of all instances of this class (e.g. Person.find_all)
142    # (always returns collection)
143    def Resource.find_all(*args)
144      find(:all, *args)
145    end
146   
147    def Resource.find(*args)
148      class_uri.find(*args)
149    end
150   
151    #####                         #####
152    ##### instance level methods        #####
153    #####                         #####
154    def find(*args)
155      # extract sort options from args
156      options = args.last.is_a?(Hash) ? args.pop : {}
157     
158      query = Query.new.distinct(:s)
159      query.where(:s, Namespace.lookup(:rdf,:type), self)
160     
161      if options.include? :order
162        sort_predicate = options[:order]
163        query.sort(:sort_value)
164        query.where(:s, sort_predicate, :sort_value)
165      end
166     
167      if options.include? :reverse_order
168        sort_predicate = options[:reverse_order]
169        query.reverse_sort(:sort_value)
170        query.where(:s, sort_predicate, :sort_value)
171      end
172     
173      if options.include? :where
174        raise ActiveRdfError, "where clause should be hash of predicate => object" unless options[:where].is_a? Hash
175        options[:where].each do |p,o|
176          if options.include? :context
177            query.where(:s, p, o, options[:context])
178          else
179            query.where(:s, p, o)
180          end
181        end
182      else
183        if options[:context]
184          query.where(:s, :p, :o, options[:context])
185        end
186      end
187     
188      query.limit(options[:limit]) if options[:limit]
189      query.offset(options[:offset]) if options[:offset]
190     
191      if block_given?
192        query.execute do |resource|
193          yield resource
194        end
195      else
196        query.execute(:flatten => false)
197      end
198    end
199    def self.find_all_predicates   
200     
201      $all_predicates_array = Query.new.distinct(:s).where(:s,Namespace.lookup(:rdf,:type),Namespace.lookup(:rdf,:Property)).execute if $all_predicates_array == nil
202    end
203    def localname     
204      Namespace.localname(self)
205    end
206   
207    # manages invocations such as eyal.age
208    def method_missing(method, *args)
209      # possibilities:
210      # 1. eyal.age is a property of eyal (triple exists <eyal> <age> "30")
211      # evidence: eyal age ?a, ?a is not nil (only if value exists)
212      # action: return ?a
213      #
214      # 2. eyal's class is in domain of age, but does not have value for eyal
215      # explain: eyal is a person and some other person (not eyal) has an age
216      # evidence: eyal type ?c, age domain ?c
217      # action: return nil
218      #
219      # 3. eyal.age = 30 (setting a value for a property)
220      # explain: eyal has (or could have) a value for age, and we update that value
221      # complication: we need to find the full URI for age (by looking at
222      # possible predicates to use
223      # evidence: eyal age ?o  (eyal has a value for age now, we're updating it)
224      # evidence: eyal type ?c, age domain ?c (eyal could have a value for age, we're setting it)
225      # action: add triple (eyal, age, 30), return 30
226      #
227      # 4. eyal.age is a custom-written method in class Person
228      # evidence: eyal type ?c, ?c.methods includes age
229      # action: inject age into eyal and invoke
230      #
231      # 5. eyal.age is registered abbreviation
232      # evidence: age in @predicates
233      # action: return object from triple (eyal, @predicates[age], ?o)
234      #
235      # 6. eyal.foaf::name, where foaf is a registered abbreviation
236      # evidence: foaf in Namespace.
237      # action: return namespace proxy that handles 'name' invocation, by
238      # rewriting into predicate lookup (similar to case (5)
239     
240      $activerdflog.debug "method_missing: #{method}"
241     
242     
243      # are we doing an update or not?
244      # checking if method ends with '='
245     
246      update = method.to_s[-1..-1] == '='
247      methodname = if update
248        method.to_s[0..-2]
249      else
250        method.to_s
251      end
252     
253      # extract single values from array unless user asked for eyal.all_age
254      flatten = true
255      if method.to_s[0..3] == 'all_'
256        flatten = false
257        methodname = methodname[4..-1]
258      end
259     
260      # check possibility (5)
261      if @predicates.include?(methodname)
262        if update
263          return set_predicate(@predicates[methodname], args)
264        else
265          return get_predicate(@predicates[methodname])
266        end
267      end
268     
269      # check possibility (6)
270      if Namespace.abbreviations.include?(methodname.to_sym)
271        namespace = Object.new 
272        @@uri = methodname
273        @@subject = self
274        @@flatten = flatten
275       
276        # catch the invocation on the namespace
277        class <<namespace
278          def method_missing(localname, *values)
279            update = localname.to_s[-1..-1] == '='
280            predicate = if update
281                          Namespace.lookup(@@uri, localname.to_s[0..-2])
282                        else
283                          Namespace.lookup(@@uri, localname)
284                        end
285           
286            if update
287              @@subject.set_predicate(predicate, values)
288            else
289              @@subject.get_predicate(predicate, @@flatten)
290            end
291          end
292          private(:type)
293        end
294        return namespace
295      end
296
297      $all_predicates_array.each do |pred|
298        if Namespace.localname(pred) == methodname
299          if update
300            return set_predicate(pred, args)
301          else
302            return get_predicate(pred, flatten)
303          end
304        end
305      end
306
307      candidates = if update
308                      (class_level_predicates + direct_predicates).compact.uniq
309                    else
310                      direct_predicates
311                    end
312
313                        # checking possibility (1) and (3)
314                        candidates.each do |pred|
315                                if Namespace.localname(pred) == methodname
316          if update
317            return set_predicate(pred, args)
318          else
319            return get_predicate(pred, flatten)
320          end
321                                end
322                        end
323                       
324                        raise ActiveRdfError, "could not set #{methodname} to #{args}: no suitable
325                        predicate found. Maybe you are missing some schema information?" if update
326
327                        # get/set attribute value did not succeed, so checking option (2) and (4)
328                       
329                        # checking possibility (2), it is not handled correctly above since we use
330                        # direct_predicates instead of class_level_predicates. If we didn't find
331                        # anything with direct_predicates, we need to try the
332                        # class_level_predicates. Only if we don't find either, we
333                        # throw "method_missing"
334                        candidates = class_level_predicates
335
336                        # if any of the class_level candidates fits the sought method, then we
337                        # found situation (2), so we return nil or [] depending on the {:array =>
338                        # true} value
339                        if candidates.any?{|c| Namespace.localname(c) == methodname}
340                                return_ary = args[0][:array] if args[0].is_a? Hash
341                                if return_ary
342                                        return []
343                                else
344                                        return nil
345                                end
346                        end
347
348                        # checking possibility (4)
349                        # TODO: implement search strategy to select in which class to invoke
350                        # e.g. if to_s defined in Resource and in Person we should use Person
351                        $activerdflog.debug "RDFS::Resource: method_missing option 4: custom class method"
352                        self.type.each do |klass|
353                                if klass.instance_methods.include?(method.to_s)
354                                        _dup = klass.new(uri)
355                                        return _dup.send(method,*args)
356                                end
357                        end
358
359                        # if none of the three possibilities work out, we don't know this method
360                        # invocation, but we don't want to throw NoMethodError, instead we return
361                        # nil, so that eyal.age does not raise error, but returns nil. (in RDFS,
362                        # we are never sure that eyal cannot have an age, we just dont know the
363                        # age right now)
364                        nil
365                end
366
367                # saves instance into datastore
368                def save
369                        db = ConnectionPool.write_adapter
370                        rdftype = Namespace.lookup(:rdf, :type)
371                        types.each do |t|
372                                db.add(self, rdftype, t)
373                        end
374
375                        Query.new.distinct(:p,:o).where(self, :p, :o).execute do |p, o|
376                                db.add(self, p, o)
377                        end
378                end
379
380                # returns all rdf:type of this instance, e.g. [RDFS::Resource,
381                # FOAF::Person]
382                #
383                # Note: this method performs a database lookup for { self rdf:type ?o }. For
384                # simple type-checking (to know if you are handling an ActiveRDF object, use
385                # self.class, which does not do a database query, but simply returns
386                # RDFS::Resource.
387                def type
388                        types.collect do |type|
389                                ObjectManager.construct_class(type)
390                        end
391                end
392
393    # define a localname for a predicate URI
394    #
395    # localname should be a Symbol or String, fulluri a Resource or String, e.g.
396    # add_predicate(:name, FOAF::lastName)
397    def add_predicate localname, fulluri
398                        localname = localname.to_s
399                        fulluri = RDFS::Resource.new(fulluri) if fulluri.is_a? String
400
401                        # predicates is a hash from abbreviation string to full uri resource
402                        @predicates[localname] = fulluri
403                end
404
405
406                # overrides built-in instance_of? to use rdf:type definitions
407                def instance_of?(klass)   
408   
409    if klass.to_s == BNode.to_s || self.class.to_s == BNode.to_s
410     
411      return (klass.to_s == self.class.to_s) ? true : false
412    else
413      self.type.include?(klass)
414    end
415    end
416
417                # returns all predicates that fall into the domain of the rdf:type of this
418                # resource
419                def class_level_predicates   
420                        type = Namespace.lookup(:rdf, 'type')
421                        domain = Namespace.lookup(:rdfs, 'domain')
422      result = Query.new.distinct(:p).where(self,type,:t).where(:p, domain, :t).execute || []
423      $all_predicates_array = $all_predicates_array | result
424      result
425                end
426
427                # returns all predicates that are directly defined for this resource
428                def direct_predicates(distinct = true)
429        query = nil     
430                        if distinct
431                                query = Query.new.distinct(:p).where(self, :p, :o)
432                        else
433                                query = Query.new.select(:p).where(self, :p, :o)
434                end
435     result = query.execute
436     $all_predicates_array = $all_predicates_array | result
437     result
438                end
439
440                def property_accessors
441                        direct_predicates.collect {|pred| Namespace.localname(pred) }
442                end
443
444                # alias include? to ==, so that you can do paper.creator.include?(eyal)
445                # without worrying whether paper.creator is single- or multi-valued
446                alias include? ==
447
448                # returns uri of resource, can be overridden in subclasses
449                def to_s
450                        "<#{uri}>"
451                end
452
453                def set_predicate(predicate, values)     
454      FederationManager.delete(self, predicate)
455      values.flatten.each {|v| FederationManager.add(self, predicate, v) }
456      #clear cache
457      $triple[self.uri]=nil 
458      values
459    end
460    def cache(flatten=false)               
461      tuple = Hash.new
462      properties = Query.new.distinct(:p,:o).where(self, :p, :o).execute(:flatten => flatten) 
463      if properties == nil 
464        $triple[self.uri]=tuple
465        return
466      end     
467      properties.each do |p,o|     
468        tuple[p] = Array.new if tuple[p] == nil
469        tuple[p] << o     
470      end
471      $triple[self.uri]=tuple
472    end
473    def get_properties     
474        cache(true) if $triple[self.uri] == nil   
475      $triple[self.uri]
476    end
477    def get_predicate(predicate, flatten=false)   
478   
479       cache(flatten) if $triple[self.uri] == nil
480      #original code
481     # values = Query.new.distinct(:o).where(self, predicate, :o).execute(:flatten => flatten)
482       values  = $triple[self.uri][predicate]!= nil && $triple[self.uri][predicate].size == 1 && flatten ?  $triple[self.uri][predicate].first : $triple[self.uri][predicate]
483     
484       values = Array.new if values == nil && flatten==false
485     
486      # TODO: fix '<<' for Fixnum values etc (we cannot use values.instance_eval
487      # because Fixnum cannot do instace_eval, they're not normal classes)
488      if values.is_a?(RDFS::Resource) and !values.nil?
489        # prepare returned values for accepting << later, eg. in
490        # eyal.foaf::knows << knud
491        #
492        # store @subject, @predicate in returned values
493        values.instance_exec(self, predicate) do |s,p|
494          @subj = s
495          @pred = p
496        end
497
498        # overwrite << to add triple to db
499        values.instance_eval do
500          def <<(value)
501            FederationManager.add(@subj, @pred, value)
502          end
503        end
504      end
505
506      values
507    end
508
509                private
510#               def ancestors(predicate)
511#                       subproperty = Namespace.lookup(:rdfs,:subPropertyOf)
512#                       Query.new.distinct(:p).where(predicate, subproperty, :p).execute
513#               end
514
515
516
517                # returns all rdf:types of this resource but without a conversion to
518                # Ruby classes (it returns an array of RDFS::Resources)
519                def types
520                        type = Namespace.lookup(:rdf, :type)
521
522                        # we lookup the type in the database
523                        #types = Query.new.distinct(:t).where(self,type,:t).execute
524      types = get_predicate(type,false)
525
526                        # we are also always of type rdfs:resource and of our own class (e.g. foaf:Person)
527                        defaults = []
528                        defaults << Namespace.lookup(:rdfs,:Resource)
529                        defaults << self.class.class_uri
530
531                        (types + defaults).uniq
532                end
533        end
534end
535
536# proxy to manage find_by_ invocations
537class DynamicFinderProxy
538  @ns = nil
539  @where = nil
540  @value = nil
541  attr_reader :value
542  private(:type)
543
544  # construct proxy from find_by text
545  # foaf::name
546  def initialize(find_string, where, *args)
547    @where = where || []
548    parse_attributes(find_string, *args)
549  end
550
551  def method_missing(method, *args)
552    # we store the ns:name for later (we need to wait until we have the
553    # arguments before actually constructing the where clause): now we just
554    # store that a where clause should appear about foaf:name
555
556    # if this method is called name_and_foaf::age we add ourself to the query
557    # otherwise, the query is built: we execute it and return the results
558    if method.to_s.include?('_and_')
559      parse_attributes(method.to_s, *args)
560    else
561      @where << Namespace.lookup(@ns, method.to_s)
562      query(*args)
563    end
564  end
565
566  private 
567  # split find_string by occurrences of _and_
568  def parse_attributes string, *args
569    attributes = string.split('_and_')
570    attributes.each do |atr|
571      # attribute can be:
572      # - a namespace prefix (foaf): store prefix in @ns to prepare for method_missing
573      # - name (attribute name): store in where to prepare for method_missing
574      if Namespace.abbreviations.include?(atr.to_sym)
575        @ns = atr.to_s.downcase.to_sym
576      else
577        # found simple attribute label, e.g. 'name'
578        # find out candidate (full) predicate for this localname: investigate
579        # all possible predicates and select first one with matching localname
580        candidates = Query.new.distinct(:p).where(:s,:p,:o).execute
581        @where << candidates.select {|cand| Namespace.localname(cand) == atr}.first
582      end
583    end
584
585    # if the last attribute was a prefix, return this dynamic finder (we'll
586    # catch the following method_missing and construct the real query then)
587    # if the last attribute was a localname, construct the query now and return
588    # the results
589    if Namespace.abbreviations.include?(attributes.last.to_sym)
590      return self
591    else
592      return query(*args)
593    end
594  end
595 
596  # construct and execute finder query
597  def query(*args)
598    # extract options from args or use an empty hash (no options given)
599    options = args.last.is_a?(Hash) ? args.last : {}
600
601    # build query
602    query = Query.new.distinct(:s)
603    @where.each_with_index do |predicate, i|
604      # specify where clauses, use context if given
605      if options[:context]
606        query.where(:s, predicate, args[i], options[:context])
607      else
608        query.where(:s, predicate, args[i])
609      end
610    end
611
612    # use sort order if given
613    if options.include? :order
614      sort_predicate = options[:order]
615      query.sort(:sort_value)
616      # add sort predicate where clause unless we have it already
617      query.where(:s, sort_predicate, :sort_value) unless @where.include? sort_predicate
618    end
619
620    if options.include? :reverse_order
621      sort_predicate = options[:reverse_order]
622      query.reverse_sort(:sort_value)
623      query.where(:s, sort_predicate, :sort_value) unless @where.include? sort_predicate
624    end
625
626    query.limit(options[:limit]) if options[:limit]
627    query.offset(options[:offset]) if options[:offset]
628
629    $activerdflog.debug "executing dynamic finder: #{query.to_sp}"
630
631    # store the query results so that caller (Resource.method_missing) can
632    # retrieve them (we cannot return them here, since we were invoked from
633    # the initialize method so all return values are ignored, instead the proxy
634    # itself is returned)
635    @value = query.execute
636    return @value
637  end
638end
Note: See TracBrowser for help on using the browser.