Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
351 lines (310 sloc) 10.7 KB
require 'digest'
require 'set'
module RDF
# Represents an RDF property.
# If an optional subject is provided on instatiation, provides access to values of this property belonging to the
# associated subject. Property objects are +Enumerable+. Values are returned as copies with no order garunteed
# and may be accessed individually by key.
#
# == Usage
# age = RDF::Property.new('http://activerdf.org/test/age')
# age.domain => [#<RDFS::Resource @uri="http://activerdf.org/test/Person">]
# age.range => [#<RDFS::Resource @uri="http://www.w3.org/1999/02/22-rdf-syntax-ns#Literal">]
# age.type => [#<RDFS::Resource @uri="http://www.w3.org/1999/02/22-rdf-syntax-ns#Property">]
# age.to_a => ActiveRdfError: http://activerdf.org/test/age: no associated subject
#
# email = RDF::Property.new('http://activerdf.org/test/email', 'http://activerdf.org/test/eyal')
# email.replace("eyal@cs.vu.nl") # replace any existing values
# email.add("eyal.oren@deri.com") # add new value to this property
# email += ("eyal.oren@deri.net") # alternative way to add new value
# email.clear # delete any existing values
# email.add(["eyal@cs.vu.nl","eyal.oren@deri.com"]) # add array containing values
# email["eyal.oren@deri.com"] = "eyal.oren@deri.net" # replace existing value
# email[p.index("eyal.oren@deri.net")] = "eyal.oren@deri.org" # replace existing value by key
# email.include?("eyal.oren@deri.org") => true # check for existing value
# email == ["eyal.oren@deri.org","eyal@cs.vu.nl"] => true # compare value(s) to array (order is ignored)
# email.delete("eyal@cs.vu.nl") # delete specific value
# email == "eyal.oren@deri.org" => true # compare value(s) to single value
# email.collect!{|val| val.gsub(/@/,' at ').gsub(/\./,' dot ')} # update value(s) with result of block
class Property < RDFS::Resource
attr_reader :subject
def initialize(property, subject = nil)
super(property)
@subject = subject
@lang = nil
@exact_lang = true
@datatype = nil
@context = nil
if @subject
class<<self
include AssociatedProperty
end
end
end
self.class_uri = ActiveRDF::Namespace.lookup(:rdf, :Property)
def initialize_copy(property)
if @subject
class<<self
include AssociatedProperty
end
end
end
# Returns the property object for this property without @subject set
def property
RDF::Property.new(self)
end
end
# Provides methods for accessing property values when @subject is set
module AssociatedProperty
include ActiveRDF::Helpers
include Enumerable
# Value reference. Retrieves a copy of the value by the key or value. Returns nil if not found.
def [](md5_or_value)
unless md5_or_value.nil?
arr = to_a
arr.find{|value| value == md5_or_value} || arr.find{|value| get_key(value) == md5_or_value}
end
end
alias :at :[]
# Selective value replacement. Replaces the value given by key or value. Raises IndexError if value not found.
def []=(md5_or_value,new_value)
value = self[md5_or_value]
raise IndexError, "Couldn't find existing value to replace: #{md5_or_value}" unless value
ActiveRDF::FederationManager.delete(@subject, self.property, value)
add(new_value)
end
# Returns a new array with the object appended, or the objects values if obj.respond_to? :to_ary
def +(obj)
to_a + [*obj]
end
# Returns a new array with the object removed
def -(obj)
to_a - [*obj]
end
def ==(other)
if other.respond_to?(:to_ary)
Set.new(other) == Set.new(to_a)
else
arr = to_a
if arr.size == 1
other == arr[0] || super
else
super
end
end
end
alias :eql? :==
# Append. Adds the given object(s) to the values for this property belonging to @subject
# This expression returns the property itself, so several appends may be chained together.
def add(*args)
args.each do |arg|
if arg.respond_to?(:to_ary)
arg.to_ary.each {|item| ActiveRDF::FederationManager.add(@subject, self.property, item)}
else
ActiveRDF::FederationManager.add(@subject, self.property, arg)
end
end
self
end
# Removes all values
def clear
ActiveRDF::FederationManager.delete(@subject, self.property)
self
end
# Invokes the block once for each value, replacing the value with the value returned by block
def collect!(&block)
to_a.each do |item|
new_item = yield(item)
delete(item)
add(new_item)
end
self
end
alias :map! :collect!
# Returns the context for the property if context is nil.
# Returns a new RDF::Property object with the @context value set if context is provided
# see also #lang, #datatype
def context(context = nil)
if context.nil?
@context
else
property_with_context = self.dup
property_with_context.context = context
property_with_context
end
end
# Sets context for this property
def context=(context)
@context = context
end
# Returns the datatype if type is nil.
# Returns a new RDF::Property object with the @datatype set if type is provided
# see also #context, #lang
def datatype(type = nil)
if type.nil?
@datatype
else
property_with_datatype = dup
property_with_datatype.datatype = type
property_with_datatype
end
end
# Sets datatype for this property
def datatype=(type)
@datatype = type
end
# Deletes value given by key or value. If the item is not found, returns nil.
# If the optional code block is given, returns the result of block if the item is not found
def delete(md5_or_value)
value = self[md5_or_value]
if value
ActiveRDF::FederationManager.delete(@subject, self.property, value)
value
elsif block_given?
yield
end
end
# Deletes every value for which block evaluates to true
def delete_if(&block) # :yields: key, value
reject!(&block)
self
end
# Calls block once for each value, passing a copy of the value as a parameter
def each(&block) # :yields: value
q = ActiveRDF::Query.new.distinct(:o).where(@subject,self,:o,@context)
if @lang and !@datatype
q.lang(:o,@lang,@exact_lang)
elsif @datatype and !@lang
q.datatype(:o, @datatype)
elsif @lang and @datatype
raise ActiveRdfError, "@datatype and @lang may not both be set"
end
q.execute(&block)
self
end
alias :each_value :each
# Calls block once for each value, passing the key as a parameter
def each_key(&block) # :yields: key
keys.each(&block)
self
end
# Calls block once for each value, passing the key and value as a parameters
# See also #to_h
def each_pair(&block) # :yields: key, value
each{|value| yield get_key(value), value}
self
end
# Returns true if the property contains no values
def empty?
to_a.empty?
end
alias :blank? :empty?
# Returns a value from the property for the given key. If the key can't be found, there are several options:
# With no other arguments, it will raise an IndexError exception; if default is given, then that will be returned;
# if the optional code block is specified, then that will be run and its result returned.
def fetch(md5, default = nil, &block)
val = self[md5]
if val
val
else
if block_given?
yield md5
elsif default
default
else
raise IndexError, "could not find #{md5}"
end
end
end
# Returns true if the given key or value is present
def include?(obj)
!!self[obj]
end
alias :value? :include?
alias :has_value? :include?
# Returns the key for a given value. If not found, returns nil.
def index(obj)
value = to_a.find{|val| obj == val}
get_key(value) if value
end
# Return the value(s) of this property as a string.
def inspect
"#<RDF::Property #{abbr} [#{to_a.collect{|obj| obj.inspect}.join(", ")}]>"
end
# Returns a new array populated with the keys to the values
def keys
collect{|value| get_key(value)}
end
# Returns the language tag and the match settings for the property if tag is nil.
# Returns a new RDF::Property object with the @lang value set if tag is provided
# see also #context, #datatype
def lang(tag = nil, exact = true)
if tag.nil?
[@lang,@exact_lang]
else
property_with_lang = RDF::Property.new(self, @subject)
property_with_lang.lang = tag, exact
property_with_lang
end
end
# Sets lang and match settings
def lang=(*args)
args.flatten!
@lang = args[0].sub(/^@/,'')
@exact_lang = to_boolean(args[1],true)
end
# Returns the number of values assigned to this property for this @subject
def length
to_a.length
end
alias :size :length
# Ensure the return of only one value assigned to this property for this @subject.
# If more than 1 value is found, ActiveRdfError is thrown.
def only
entries = self.entries
raise ActiveRDF::ActiveRdfError if entries.size > 1
entries[0]
end
# Equivalent to Property#delete_if, but returns nil if no changes were made
def reject!(&block) # :yields: key, value
change = false
each_pair do |key, value|
if yield(key, value)
delete(value)
change = true
end
end
self if change
end
# Value replacement. Replaces all current value(s) with the new value
def replace(new)
clear
add(new)
self
end
alias :size :length
# Allow this Property to be automatically converted to array
alias :to_ary :to_a
# Returns a hash of copies of all values with indexes.
# Changes to this hash will not effect the underlying values. Use #add or #replace to persist changes.
# See also #each_pair
def to_h
hash = {}
to_a.each do |value|
hash[get_key(value)] = value
end
hash
end
def to_s
to_a.join(",")
end
# Return an array containing the values for the given keys.
def values_at(*args)
args.collect{|md5| self[md5]}
end
private
def get_key(value)
Digest::MD5.hexdigest(value.to_s)
end
end
end