module TaliaCore module ActiveSourceParts # Class methods for ActiveSource: # # * Property definitions for source classes (singular_property, multi_property, manual_property) # * Logic for the creation of new sources, and things like exists? # * "Import" methods for the class: create_from_xml, create_multi_from # * autofill_uri logic # * Various utility method module ClassMethods # Accessor for additional rdf types that will automatically be added to each # object of that Source class def additional_rdf_types @additional_rdf_types ||= [] end # New method for ActiveSources. If a URL of an existing Source is given as the only parameter, # that source will be returned. This makes the class work smoothly with our ActiveRDF version # query interface. # # Note that any semantic properties that were passed in to the constructor will be assigned # *after* the ActiveRecord "create" callbacks have been called. # # The option hash may contain a "files" option, which can be used to add data files directly # on creation. This will call the attach_files method on the object. def new(*args) the_source = if((args.size == 1) && (args.first.is_a?(Hash))) options = args.first options.to_options! # We have an option hash to init the source files = options.delete(:files) options[:uri] = uri_string_for(options[:uri], false) if(autofill_overwrites?) options[:uri] = auto_uri elsif(autofill_uri?) options[:uri] ||= auto_uri end attributes = split_attribute_hash(options) the_source = super(attributes[:db_attributes]) the_source.add_semantic_attributes(false, attributes[:semantic_attributes]) the_source.attach_files(files) if(files) the_source elsif(args.size == 1 && ( uri_s = uri_string_for(args[0]))) # One string argument should be the uri # Either the current object from the db, or a new one if it doesn't exist in the db find(:first, :conditions => { :uri => uri_s } ) || super(:uri => uri_s) elsif(args.size == 0 && autofill_uri?) auto = auto_uri raise(ArgumentError, "Record already exists #{auto}") if(ActiveSource.exists?(auto)) super(:uri => auto) else # In this case, it's a generic "new" call super end the_source.add_additional_rdf_types if(the_source.new_record?) the_source end # Retrieves a new source with the given type. This gets a propety hash # like #new, but it will correctly initialize a source of the type given # in the hash. If no type is given, this will create a plain ActiveSource. def create_source(args) args.to_options! type = args.delete(:type) || 'TaliaCore::ActiveSource' klass = type.constantize klass.new(args) end # Create sources from XML. The result is either a single source or an Array # of sources, depending on wether the XML contains multiple sources. # # The imported sources will be saved during import, to ensure that relations # between them are resolved correctly. If one of the imported elements # does already exist, the existing source will be rewritten using ActiveSource#rewrite_attributes # # The options may contain: # # [*reader*] The reader class that the import should use # [*progressor*] The progress reporting object, which must respond to run_with_progress(message, size, &block) # [*errors*] If given, all erors will be looged to this array instead of raising # an exception. See the create_multi_from method for more. # [*duplicates*] How to treat alredy existing sources. See ImportJobHelper for more # documentation # [*base_file_uri*] The base uri to import file from def create_from_xml(xml, options = {}) options.to_options! options.assert_valid_keys(:reader, :progressor, :errors, :duplicates, :base_file_uri) reader = options[:reader] ? options.delete(:reader).to_s.classify.constantize : TaliaCore::ActiveSourceParts::Xml::SourceReader source_properties = reader.sources_from(xml, options[:progressor], options.delete(:base_file_uri)) self.progressor = options.delete(:progressor) sources = create_multi_from(source_properties, options) (sources.size > 1) ? sources : sources.first end # Creates multiple sources from the given array of attribute hashes. The # sources are saved during import, ensuring that the relations are resolved # correctly. # # Options: # [*errors*] If given, all erors will be logged to this array instead of raising # an exception. Each "entry" in the error array will be an Error object # containing the origianl stack trace of the error # [*duplicates*] Indicates how to deal with sources that already exist in the # datastore. See the ImportJobHelper class for a documentation of # this option. Default is :skip def create_multi_from(sources, options = {}) options.to_options! options.assert_valid_keys(:errors, :duplicates) source_objects = [] run_with_progress('Writing imported', sources.size) do |progress| source_objects = sources.collect do |props| props.to_options! src = nil begin props[:uri] = uri_string_for(props[:uri], false) assit(props[:uri], "Must have a valid uri at this step") if(src = ActiveSource.find(:first, :conditions => { :uri => props[:uri] })) src.update_source(props, options[:duplicates]) else src = ActiveSource.create_source(props) end src.save! rescue Exception => e if(options[:errors]) err = Errors::ImportError.new("ERROR during import of #{props[:uri]}: #{e.message}") err.set_backtrace(e.backtrace) options[:errors] << err TaliaCore.logger.warn("Problems importing #{props[:uri]} (logged): #{e.message}") else raise end end progress.inc src end end source_objects end # This method is slightly expanded to allow passing uris and uri objects # as an "id" def exists?(value) if(uri_s = uri_string_for(value)) super(:uri => uri_s) else super end end # Semantic version of ActiveRecord::Base#update - the id may be a record id or an URL, # and the attributes may contain semantic attributes. See the update_attributes method # for details on how the semantic attributes behave. def update(id, attributes) record = find(id) raise(ActiveRecord::RecordNotFound) unless(record) record.update_attributes(attributes) end # Like update, only that it will overwrite the given attributes instead # of adding to themÆ’ def rewrite(id, attributes) record = find(id) raise(ActiveRecord::RecordNotFound) unless(record) record.rewrite_attributes(attributes) end # The pagination will also use the prepare_options! to have access to the # advanced finder options def paginate(*args) prepare_options!(args.last) if(args.last.is_a?(Hash)) super end # If will return itself unless the value is a SemanticProperty, in which # case it will return the property's value. def value_for(thing) thing.is_a?(SemanticProperty) ? thing.value : thing end # Returns true if the given attribute is one that is stored in the database def db_attr?(attribute) db_attributes.include?(attribute.to_s) end # Tries to expand a generic URI value that is either given as a full URL # or a namespace:name value. # # This will assume a full URL if it finds a ":/" string inside the URI. # Otherwise it will construct a namespace - name URI def expand_uri(uri) # TODO: Merge with uri_for ? assit_block do |errors| unless(uri.respond_to?(:uri) || uri.kind_of?(String)) || uri.kind_of?(Symbol) errors << "Found strange object of type #{uri.class}" end true end uri = uri.respond_to?(:uri) ? uri.uri.to_s : uri.to_s return uri if(uri.include?(':/')) N::URI.make_uri(uri).to_s end # Splits the attribute hash that is given for new, update and the like. This # will return another hash, where result[:db_attributes] will contain the # hash of the database attributes while result[:semantic_attributes] will # contain the other attributes. # # The semantic attributes will be expanded to full URIs whereever possible. # # This method will *not* check for attributes that correspond to singular # property names. def split_attribute_hash(attributes) assit_kind_of(Hash, attributes) db_attributes = {} semantic_attributes = {} attributes.each do |field, value| if(db_attr?(field)) db_attributes[field] = value elsif(defined_property?(field)) semantic_attributes[field] = value else semantic_attributes[expand_uri(field)] = value end end { :semantic_attributes => semantic_attributes, :db_attributes => db_attributes } end def property_options_for(property) property = defined_props[property.to_s] if(defined_props[property.to_s]) this_options = my_property_options[property.to_s] parent_options = superclass.try_call.property_options_for(property) if(this_options && parent_options) parent_options.merge(this_options) else this_options || parent_options || {} end end def defined_property?(prop_name) defined_props.include?(prop_name.to_s) || superclass.try_call.defined_property?(prop_name.to_s) end # All the options that should be destroy for :dependent => :destroy settings def props_to_destroy to_destroy = (superclass.try_call.props_to_destroy || []) my_property_options.each do |prop, options| to_destroy << prop if(options[:dependent] == :destroy) end to_destroy end private # Make URL for autofilling def auto_uri (N::LOCAL + self.name.tableize + "/#{rand Time.now.to_i}").to_s end # The attributes stored in the database def db_attributes @db_attributes ||= (ActiveSource.new.attribute_names << 'id') end # Helper to define a "additional type" in subclasses which will # automatically be added on Object creation def has_rdf_type(*types) @additional_rdf_types ||= [] types.each { |t| @additional_rdf_types << t.to_s } end # Class helper to declare that this Source model is allowed to automatically # create uri values for new elements. In that case, the model will # automatically assign a URL to all new records to which no url value has # been passed. # # If the :force option is set, the autofill will overwrite an existing uri that # is passed in during creation. def autofill_uri(options = {}) options.to_options! options.assert_valid_keys(:force) @can_autofill = true @autofill_overwrites = options[:force] end def autofill_uri? @can_autofill end def autofill_overwrites? @autofill_overwrites end def singular_property(prop_name, property, options = {}) define_property(prop_name, property, options.merge(:singular_property => true)) end # Defines a multi-value property in the same way as #singular_property def multi_property(prop_name, property, options = {}) define_property(prop_name, property, options.merge(:singular_property => false)) end # Defines a "manual" property. This means that getters and setters are provided # by the user and this statement only declares that the system may autoassign to # that property def manual_property(prop_name) defined_props[prop_name.to_s] = :manual end # Helper to define a "singular accessor" for something (e.g. siglum, catalog) # This accessor will provide an "accessor" method that returns the # single property value directly and an assignment method that replaces # the property with the value. # # A find_by_<property> finder method is also created. # # The Source will cache newly set singular properties internally, so that # the new value is immediately reflected on the object. However, the # change will only be made permanent on #save! - and saving will also clear # the cache # # [*:dependent*] You may pass :dependend => :destroy as for ActiveRecord relations def define_property(prop_name, property, options = {}) prop_name = prop_name.to_s property_options(property, options) # Save options for the current property return if(defined_props.include?(prop_name)) raise(ArgumentError, "Cannot overwrite method #{prop_name}") if(self.instance_methods.include?(prop_name) || self.instance_methods.include?("#{prop_name}=")) # define the accessor define_method(prop_name) do self[property] end # define the writer define_writer(prop_name, property) # define the finder (class << self ; self; end).module_eval do define_method("find_by_#{prop_name}") do |value, *optional| raise(ArgumentError, "Too many options") if(optional.size > 1) options = optional.last || {} finder = options.merge( :find_through => [property, value] ) find(:all, finder) end end defined_props[prop_name] = property end # Helper to dynamically define the singular or multi-value assignment accessor def define_writer(prop_name, property) define_method("#{prop_name}=") do |values| self[property] = values end end # The hash containing the mapping between defined property names and the # RDF properties on which they are defined. def defined_props @defined_props ||= {} end # Hash that contains all options that are defined for the properties def my_property_options @my_property_options ||= {} end # Sets the options for handling semantic relations/properties with the predicate # #property. The options are: # # [*force_relation*] Forces the the values to be relations. This means that # each and every value passed to the generated accessors # will be interpreted as a URL. *DEPRECATED*, use # `:type => TaliaCore::ActiveSource` instead # [*type*] Declare that the values of this property should be of the given # type, which should be a Ruby runtime class or a symbol corresponding # to an ActiveRecord field type. If this is an ActiveSource subclass, # this will force all values that are passed to this property # to be interpreted as the URI of an ActiveSource (if the value is not # an ActiveSource already) # [*singular_property*] # be used to force each value passed to the accessors, #new and # #update* will be interpreted as the uri of a source of the given type, # def property_options(property, options) options.to_options! options.assert_valid_keys(:force_relation, :dependent, :type, :singular_property) if(force = options.delete(:force_relation).true?) warn("Deprecation Warning: :force_relation is deprecated - use ':type => TaliaCore::ActiveSource' instead") options[:type] ||= ActiveSource end my_property_options[property.to_s] ||= {} my_property_options[property.to_s].merge!(options) end # This gets the URI string from the given value. This will just return # the value if it's a string. It will return the result of value.uri, if # that method exists; otherwise it'll return nil # # If the id_aware flag is set this will return nil for any uri string that # appears to be a numeric id. def uri_string_for(value, id_aware = true) result = if value.is_a? String return nil if((value =~ /\A\d+(-.*)?\Z/) && id_aware) # This looks like a record id or record param, encoded as a string # if this is a local name, prepend the local namespace (value =~ /:/) ? value : (N::LOCAL + value).uri elsif(value.respond_to?(:uri)) value.uri else id_aware ? nil : value end result = result.to_s if result result end end end end