module TaliaCore # This class encapsulate the basic "Source" behaviour for an element in the # semantic store. This is the baseclass for all things that are represented # as an "Resource" (with URL) in the semantic store. # # If an object is modified but <b>not</b> saved, the ActiveSource does <b>not</b> # guarantee that the RDF will always be in sync with the database. However, # a subsequent saving of the object will re-sync the RDF. # # An effort is made to treat RDF and database writes in the same way, so that # they should <i>usually</i> be in sync: # # * Write operations are usually lazy, the data should only be saved once # save! is called. However, ActiveRecord may still decide that the objects # need to be saved if they are involved in other operations. # * Operations that clear a predicate are <b>immediate</b>. That also means # that using singular property setter, if # used will immediately erase the old value. If the record is not saved, # the property will be left empty (and not revert to the original value!) class ActiveSource < ActiveRecord::Base # Act like an ActiveRdfResource include RDFS::ResourceLike extend ActiveSourceParts::ClassMethods extend ActiveSourceParts::Finders extend ActiveSourceParts::SqlHelper include ActiveSourceParts::PredicateHandler extend ActiveSourceParts::PredicateHandler::ClassMethods include ActiveSourceParts::Rdf extend TaliaUtil::Progressable # Progress for import methods on class # Set the handlers for the callbacks defined in the other modules. The # order matters here. after_update :auto_update_rdf after_create :auto_create_rdf after_save :save_wrappers # Save the cache wrappers # Relations where this source is the subject of the triple has_many :semantic_relations, :foreign_key => 'subject_id', :class_name => 'TaliaCore::SemanticRelation', :dependent => :destroy # Data records attached to the active source has_many :data_records, :class_name => 'TaliaCore::DataTypes::DataRecord', :dependent => :destroy, :foreign_key => 'source_id' # Relations where this source is the object of the relation has_many :related_subjects, :foreign_key => 'object_id', :class_name => 'TaliaCore::SemanticRelation' has_many :subjects, :through => :related_subjects validates_format_of :uri, :with => /\A\S*:.*\Z/, :message => '<{{value}}> does not look like an uri.' validates_uniqueness_of :uri before_destroy :remove_inverse_properties # Remove inverse properties when destroying an element # We may wish to use the following Regexp. # It matches: # http://discovery-project.eu # http://www.discovery-project.eu # http://trac.talia.discovery-project.eu # http://trac.talia.discovery-project.eu/source # http://trac.talia.discovery-project.eu/namespace#predicate # # validates_format_of :uri, # :with => /^(http|https):\/\/[a-z0-9\_\-\.]*[a-z0-9_-]{1,}\.[a-z]{2,4}[\/\w\d\_\-\.\?\&\#]*$/i validate :check_uri # Uri in short notation def short_uri N::URI.new(self.uri).to_name_s end # Helper def value_for(thing) self.class.value_for(thing) end # To string: Just return the URI. Use to_xml if you need something more # involved. def to_s self[:uri] end # Create a new uri object def to_uri self[:uri].to_uri end # Works in the normal way for database attributes. If the value # is not an attribute, it tries to find objects related to this source # with the value as a predicate URL and returns a collection of those. # # The assignment operator remains as it is for the ActiveRecord. def [](attribute) if(db_attr?(attribute)) super(attribute) else get_objects_on(attribute) end end alias :get_attribute :[] # Assignment to an attribute. This will overwrite all current triples. def []=(attribute, value) if(db_attr?(attribute)) super(attribute, value) else pred = get_attribute(attribute) pred.remove pred << value end end # Make aliases for the original updating methods alias :update_attributes_orig :update_attributes alias :update_attributes_orig! :update_attributes! # Updates the source with the given properties. The 'mode' field indicates if # and how the update will be performed. See the ImportJobHelper class for # the different modes. # # As opposed to the *_attributes method, this will also handle file elements. # The default mode is :skip (do nothing) def update_source(properties, mode) properties.to_options! mode = :update if(self.is_a?(SourceTypes::DummySource)) # Dummy sources are always updated mode ||= :skip mode = mode.to_sym return self if(mode == :skip) # If we're told to ignore updates # Deal with already existing sources files = properties.delete(:files) if(mode == :overwrite) # If we are to overwrite, delete all relations and update normally self.semantic_relations.destroy_all self.data_records.destroy_all mode = :update elsif(mode == :update && files && self.data_records.size > 0) # On updating we should only remove the files if there are new ones self.data_records.destroy_all end # Add any files attach_files(files) if(files) # Rewrite the type, if neccessary type = properties[:type] switch_type = type && (self.type != type) # Warn to the log if we have a problematic type change TaliaCore.logger.warn("WARNING: Type change from #{self.type} to #{type}") if(switch_type && !self.is_a?(SourceTypes::DummySource)) self.type = type if(switch_type) # Now we should either be adding or updating assit(mode == :update || mode == :add) update = (mode == :update) # Overwrite with or add the imported attributes update ? rewrite_attributes(properties) : update_attributes(properties) self end # Updates *all* attributes of this source. For the database attributes, this works # exactly like ActiveRecord::Base#update_attributes # # If semantic attributes are present, they will be updated on the semantic store. # # After the update, the source will be saved. def update_attributes(attributes) yield self if(block_given?) super(process_attributes(false, attributes)) end # As update_attributes, but uses save! to save the source def update_attributes!(attributes) yield self if(block_given?) super(process_attributes(false, attributes)) end # Works like update_attributes, but will replace the semantic attributes # rather than adding to them. def rewrite_attributes(attributes) yield self if(block_given?) update_attributes_orig(process_attributes(true, attributes)) end # Like rewrite_attributes, but calling save! def rewrite_attributes!(attributes) yield self if(block_given?) update_attributes_orig!(process_attributes(true, attributes)) end # Helper to update semantic attributes from the given hash. If there is a # "<value>" string, it will be treated as a reference to an URI. Hash # values may be arrays. # # If overwrite is set to yes, the given attributes (and only those) # are replaced with the values from the hash. Otherwise # the attribute values will be added to the existing ones def add_semantic_attributes(overwrite, attributes) attributes.each do |attr, value| value = [ value ] unless(value.is_a?(Array)) attr_wrap = self[attr] attr_wrap.remove if(overwrite) value.each { |val |self[attr] << target_for(val) } end end # Returns a special object which collects the "inverse" properties # of the Source - these are all RDF properties which have the current # Source as the object. # # The returned object supports the [] operator, which allows to fetch the # "inverse" (the RDF subjects) for the given predicate. # # Example: <tt>person.inverse[N::FOO::creator]</tt> would return a list of # all the elements of which the current person is the creator. def inverse inverseobj = Object.new inverseobj.instance_variable_set(:@assoc_source, self) class << inverseobj def [](property) @assoc_source.subjects.find(:all, :conditions => { 'semantic_relations.predicate_uri' => property.to_s } ) end private :type end inverseobj end # Accessor that allows to lookup a namespace/name combination. This works like # the [] method: I will return an array-like object on predicates can be # manipulated. def predicate(namespace, name) get_objects_on(get_namespace(namespace, name)) end # Setter method for predicates by namespace/name combination. This will # *add a precdicate triple, not replace one!* def predicate_set(namespace, name, value) predicate(namespace, name) << value end # Setter method that will only add the value if it doesn't exist already def predicate_set_uniq(namespace, name, value) pred = predicate(namespace, name) pred << value unless(pred.include?(value)) end # Replaces the given predicate with the value. Good for one-value predicates def predicate_replace(namespace, name, value) pred = predicate(namespace, name) pred.remove pred << value end # Gets the direct predicates (using the database) def direct_predicates raise(ActiveRecord::RecordNotFound, "Cannot do this on unsaved record.") if(new_record?) rels = SemanticRelation.find_by_sql("SELECT DISTINCT predicate_uri FROM semantic_relations WHERE subject_id = #{self.id}") rels.collect { |rel| N::Predicate.new(rel.predicate_uri) } end # Gets the inverse predicates def inverse_predicates raise(ActiveRecord::RecordNotFound, "Cannot do this on unsaved record.") if(new_record?) rels = SemanticRelation.find_by_sql("SELECT DISTINCT predicate_uri FROM semantic_relations WHERE object_id = #{self.id}") rels.collect { |rel| N::Predicate.new(rel.predicate_uri) } end # True if the given attribute is a database attribute def db_attr?(attribute) ActiveSource.db_attr?(attribute) end # Writes the predicate directly to the database and the rdf store. The # Source does not need to be saved and no data is loaded from the database. # This is faster than adding the data normally and doing a full save, # at least if only one or two predicates are written. def write_predicate_direct(predicate, value) autosave = self.autosave_rdf? value.save! if(value.is_a?(ActiveSource) && value.new_record?) self.autosave_rdf = false self[predicate] << value uri_res = N::URI.new(predicate) # Now add the RDF data by hand if(value.kind_of?(Array)) value.each do |v| my_rdf.direct_write_predicate(uri_res, v) end else my_rdf.direct_write_predicate(uri_res, value) end save! # Save without RDF save self.autosave_rdf = autosave end # XML Representation of the source. The object is saved if this is a new # record. def to_xml save! if(new_record?) ActiveSourceParts::Xml::SourceBuilder.build_source(self) end # Creates an RDF/XML resprentation of the source. The object is saved if # this is a new record. def to_rdf save! if(new_record?) ActiveSourceParts::Xml::RdfBuilder.build_source(self) end # Add the additional types to the source that were configured in the class. # Usually this will not need to be called directly, but will be automatically # called during construction. # # This will check the existing types to avoid duplication def add_additional_rdf_types # return if(self.class.additional_rdf_types.empty?) type_hash = {} self.types.each { |type| type_hash[type.respond_to?(:uri) ? type.uri.to_s : type.to_s] = true } # Add the "class" default type type (unless this is the source for the self type itself).0 self.types << rdf_selftype unless(type_hash[rdf_selftype.to_s] || (rdf_selftype.to_s == self.uri.to_s)) # Add the user-configured types self.class.additional_rdf_types.each do |type| self.types << type unless(type_hash[type.respond_to?(:uri) ? type.uri.to_s : type.to_s]) end end # Attaches files from the given hash. See the new method on ActiveSource for the # details. # # The call in this case should look like this: # # attach_files([{ url => 'url_or_filename', :options => { .. }}, ...]) # # Have a look at the DataLoader module to see how the options work. You may also provide # a single hash for :files (instead of an array) if you have just one file. Files will # be saved immediately. def attach_files(files) files = [ files ] unless(files.is_a?(Array)) files.each do |file| filename = file[:url] || file['url'] assit(filename) options = file[:options] || file['options'] || {} records = DataTypes::FileRecord.create_from_url(filename, options) records.each { |rec| self.data_records << rec } end end # This will return a list of DataRecord objects. Without parameters, this # returns all data elements on the source. If a type is given, it will # return only the elements of the given type. If both type and location are # given, it will retrieve only the specified data element def data(type = nil, location= nil) find_type = location ? :first : :all # Find just one element if a location is given type = type.name if(type.is_a?(Class)) options = {} options[:conditions] = [ "type = ?", type ] if(type && !location) options[:conditions] = [ "type = ? AND location = ?", type, location ] if(type && location) data_records.find(find_type, options) end # The RDF type that is used for objects of the current class def rdf_selftype (N::TALIA + self.class.name.demodulize) end private # Extracts the semantic attributes from the attribute hash and passes them # to add_semantic_attributes with the given overwrite flag. # The database attributes are returned by the method def process_attributes(overwrite, attributes) attributes = ActiveSource.split_attribute_hash(attributes) add_semantic_attributes(overwrite, attributes[:semantic_attributes]) attributes[:db_attributes] end # Creates the target for the given attribute. If the value # has the format <...>, it will be returned as an URI object, if it's a normal # string it will be returned as a string. def target_for(value) return value if(value.kind_of?(N::URI) || value.kind_of?(ActiveSource)) assit_block { |msg| msg << "Expected #{value.inspect} to be a String" unless(value.is_a?(String)) ; value.is_a?(String) } value.strip! if((value[0..0] == '<') && (value[-1..-1] == '>')) value = ActiveSource.expand_uri(value [1..-2]) val_src = ActiveSource.find(:first, :conditions => { :uri => value }) if(!val_src) value = SourceTypes::DummySource.new(value) value.save! else value = val_src end end value end # Get the namespace URI object for the given namespace def get_namespace(namespace, name = '') namesp_uri = N::Namespace[namespace] raise(ArgumentError, "Illegal namespace given #{namespace}") unless(namesp_uri) namesp_uri + name.to_s end # Takes over the validation for ActiveSources def validate self.class end # Check the uri should be different than N::LOCAL, this could happen when an user # leaves the input text blank. def check_uri self.errors.add(:uri, "Cannot be blank") if self.uri == N::LOCAL.to_s end # Remove the inverse properties, to be called before destroy. Doing this # through the polymorphic relation automatically could be a bit fishy... def remove_inverse_properties SemanticRelation.delete_all(["object_type = 'TaliaCore::ActiveSource' AND object_id = ?", self.id]) end end end