lib/active_fedora/base.rb in active-fedora-1.1.13 vs lib/active_fedora/base.rb in active-fedora-1.2.0

- old
+ new

@@ -1,8 +1,9 @@ require 'util/class_level_inheritable_attributes' require 'active_fedora/model' require 'active_fedora/semantic_node' +require 'solrizer/field_name_mapper' require 'nokogiri' SOLR_DOCUMENT_ID = "id" unless defined?(SOLR_DOCUMENT_ID) ENABLE_SOLR_UPDATES = true unless defined?(ENABLE_SOLR_UPDATES) @@ -28,14 +29,17 @@ # # =Implementation # This class is really a facade for a basic Fedora::FedoraObject, which is stored internally. class Base include MediaShelfClassLevelInheritableAttributes - ms_inheritable_attributes :ds_specs + ms_inheritable_attributes :ds_specs, :class_named_datastreams_desc include Model include SemanticNode - include SolrMapper + include Solrizer::FieldNameMapper + + attr_accessor :named_datastreams_desc + has_relationship "collection_members", :has_collection_member # Has this object been saved? @@ -96,14 +100,30 @@ inner_object.load_attributes_from_fedora @datastreams = datastreams_in_fedora.merge(datastreams_in_memory) end #Deletes a Base object, also deletes the info indexed in Solr, and - #the underlying inner_object. + #the underlying inner_object. If this object is held in any relationships (ie inbound relationships + #outside of this object it will remove it from those items rels-ext as well def delete + inbound_relationships(:objects).each_pair do |predicate, objects| + objects.each do |obj| + if obj.respond_to?(:remove_relationship) + obj.remove_relationship(predicate,self) + obj.save + end + end + end + Fedora::Repository.instance.delete(@inner_object) - SolrService.instance.conn.delete(self.pid) if ENABLE_SOLR_UPDATES + if ENABLE_SOLR_UPDATES + ActiveFedora::SolrService.instance.conn.delete(pid) + if defined?( Solrizer::Solrizer ) + solrizer = Solrizer::Solrizer.new + solrizer.solrize_delete(pid) + end + end end # # Datastream Management @@ -244,34 +264,251 @@ end def collection_members_remove() # will rely on SemanticNode.remove_relationship once it is implemented end + + # + # Named Datastreams management + # + def datastream_names + named_datastreams_desc.keys + end + + def add_named_file_datastream(name, file, opts={}) + opts.merge!({:blob=>file,:controlGroup=>'M'}) + add_named_datastream(name,opts) + end + # Creates a datastream with values + def add_named_datastream(name,opts={}) + + unless named_datastreams_desc.has_key?(name) && named_datastreams_desc[name].has_key?(:type) + raise "Failed to add datastream. Named datastream #{name} not defined for object #{pid}." + end + opts.merge!(named_datastreams_desc[name]) + + label = opts.has_key?(:label) ? opts[:label] : "" + #only do these steps for managed datastreams + unless (opts.has_key?(:controlGroup)&&opts[:controlGroup]!="M") + if opts.has_key?(:file) + opts.merge!({:blob => opts[:file]}) + opts.delete(:file) + end + + raise "You must define parameter blob for this managed datastream to load for #{pid}" unless opts.has_key?(:blob) + + #if no explicit label and is a file use original file name for label + if !opts.has_key?(:label)&&opts[:blob].respond_to?(:original_filename) + label = opts[:blob].original_filename + end + + if opts[:blob].respond_to?(:content_type)&&!opts[:blob].content_type.nil? && !opts.has_key?(:content_type) + opts.merge!({:content_type=>opts[:blob].content_type}) + end + + raise "The blob must respond to content_type or the hash must have :content_type property set" unless opts.has_key?(:content_type) + + #throw error for mimeType mismatch + if named_datastreams_desc[name].has_key?(:mimeType) && !named_datastreams_desc[name][:mimeType].eql?(opts[:content_type]) + raise "Content type mismatch for add datastream #{name} to #{pid}. Expected: #{named_datastreams_desc[name][:mimeType]}, Actual: #{opts[:content_type]}" + end + else + label = opts[:dsLocation] if (opts.has_key?(:dsLocation)) + end + + opts.merge!(:dsLabel => label) + + #make sure both dsid and dsID populated if a dsid is supplied + opts.merge!(:dsid=>opts[:dsID]) if opts.has_key?(:dsID) + opts.merge!(:dsID=>opts[:dsid]) if opts.has_key?(:dsid) + + ds = create_datastream(named_datastreams_desc[name][:type],opts) + #Must be of type datastream + assert_kind_of 'datastream', ds, ActiveFedora::Datastream + #make sure dsid is nil so that it uses the prefix for mapping purposes + #check dsid works for the prefix if it is set + if !ds.dsid.nil? && opts.has_key?(:prefix) + raise "dsid supplied does not conform to pattern #{opts[:prefix]}[number]" unless ds.dsid =~ /#{opts[:prefix]}[0-9]/ + end + + add_datastream(ds,opts) + end + + ######################################################################## + ### TODO: Currently requires you to update file if a managed datastream + ## but could change to allow metadata only updates as well + ######################################################################## + def update_named_datastream(name, opts={}) + #check that dsid provided matches existing datastream with that name + raise "You must define parameter dsid for datastream to update for #{pid}" unless opts.include?(:dsid) + raise "Datastream with name #{name} and dsid #{opts[:dsid]} does not exist for #{pid}" unless self.send("#{name}_ids").include?(opts[:dsid]) + add_named_datastream(name,opts) + end + + def assert_kind_of(n, o,t) + raise "Assertion failure: #{n}: #{o} is not of type #{t}" unless o.kind_of?(t) + end + + def is_named_datastream?(name) + named_datastreams_desc.has_key?(name) + end + + def named_datastreams + ds_values = {} + self.class.named_datastreams_desc.keys.each do |name| + ds_values.merge!({name=>self.send("#{name}")}) + end + return ds_values + end + + def named_datastreams_attributes + ds_values = {} + self.class.named_datastreams_desc.keys.each do |name| + ds_array = self.send("#{name}") + result_hash = {} + ds_array.each do |ds| + result_hash[ds.dsid]=ds.attributes + end + ds_values.merge!({name=>result_hash}) + end + return ds_values + end + + def named_datastreams_ids + dsids = {} + self.class.named_datastreams_desc.keys.each do |name| + dsid_array = self.send("#{name}_ids") + dsids[name] = dsid_array + end + return dsids + end + + def datastreams_attributes + ds_values = {} + self.datastreams.each_pair do |dsid,ds| + ds_values.merge!({dsid=>ds.attributes}) + end + return ds_values + end + + def named_datastreams_desc + @named_datastreams_desc ||= named_datastreams_desc_from_class + end + + def named_datastreams_desc_from_class + self.class.named_datastreams_desc + end + + def create_datastream(type,opts={}) + type.to_s.split('::').inject(Kernel) {|scope, const_name| + scope.const_get(const_name)}.new(opts) + end + + def self.has_datastream(args) + unless args.has_key?(:name) + return false + end + unless args.has_key?(:prefix) + args.merge!({:prefix=>args[:name].to_s.upcase}) + end + unless named_datastreams_desc.has_key?(args[:name]) + named_datastreams_desc[args[:name]] = {} + end + + args.merge!({:mimeType=>args[:mime_type]}) if args.has_key?(:mime_type) + + unless named_datastreams_desc[args[:name]].has_key?(:type) + #default to type ActiveFedora::Datastream + args.merge!({:type => "ActiveFedora::Datastream"}) + end + named_datastreams_desc[args[:name]]= args + create_named_datastream_finders(args[:name],args[:prefix]) + create_named_datastream_update_methods(args[:name]) + end + + def self.create_named_datastream_update_methods(name) + append_file_method_name = "#{name.to_s.downcase}_file_append" + append_method_name = "#{name.to_s.downcase}_append" + #remove_method_name = "#{name.to_s.downcase}_remove" + self.send(:define_method,:"#{append_file_method_name}") do |*args| + file,opts = *args + opts ||= {} + add_named_file_datastream(name,file,opts) + end + + self.send(:define_method,:"#{append_method_name}") do |*args| + opts = *args + opts ||= {} + #call add_named_datastream instead of add_file_named_datastream in case not managed datastream + add_named_datastream(name,opts) + end + end + + def self.create_named_datastream_finders(name, prefix) + class_eval <<-END + def #{name}(opts={}) + id_array = [] + keys = datastreams.keys + id_array = keys.select {|v| v =~ /#{prefix}\d*/} + if opts[:response_format] == :id_array + return id_array + else + named_ds = [] + id_array.each do |name| + if datastreams.has_key?(name) + named_ds.push(datastreams[name]) + end + end + return named_ds + end + end + def #{name}_ids + #{name}(:response_format => :id_array) + end + END + end + + # named datastreams desc are tracked as a hash of structure {name => args}} + def self.named_datastreams_desc + @class_named_datastreams_desc ||= {} + end + # # Relationships Management # # @returns Hash of relationships, as defined by SemanticNode # Rely on rels_ext datastream to track relationships array # Overrides accessor for relationships array used by SemanticNode. - def relationships - return rels_ext.relationships + # If outbound_only is false, inbound relationships will be included. + def relationships(outbound_only=true) + outbound_only ? rels_ext.relationships : rels_ext.relationships.merge(:inbound=>inbound_relationships) end # Add a Rels-Ext relationship to the Object. # @param predicate # @param object Either a string URI or an object that responds to .pid def add_relationship(predicate, obj) - #predicate = ActiveFedora::RelsExtDatastream.predicate_lookup(predicate) r = ActiveFedora::Relationship.new(:subject=>:self, :predicate=>predicate, :object=>obj) - rels_ext.add_relationship(r) + unless relationship_exists?(r.subject, r.predicate, r.object) + rels_ext.add_relationship(r) + #need to call here to indicate update of named_relationships + @relationships_are_dirty = true + rels_ext.dirty = true + end + end + + def remove_relationship(predicate, obj) + r = ActiveFedora::Relationship.new(:subject=>:self, :predicate=>predicate, :object=>obj) + rels_ext.remove_relationship(r) + #need to call here to indicate update of named_relationships + @relationships_are_dirty = true rels_ext.dirty = true end - def inner_object # :nodoc @inner_object end #return the pid of the Fedora Object @@ -409,19 +646,64 @@ end datastreams.each_value do |ds| # solr_doc = ds.to_solr(solr_doc) if ds.class.included_modules.include?(ActiveFedora::MetadataDatastreamHelper) ||( ds.kind_of?(ActiveFedora::RelsExtDatastream) || ( ds.kind_of?(ActiveFedora::QualifiedDublinCoreDatastream) && !opts[:model_only] ) solr_doc = ds.to_solr(solr_doc) if ds.kind_of?(ActiveFedora::MetadataDatastream) || ds.kind_of?(ActiveFedora::NokogiriDatastream) || ( ds.kind_of?(ActiveFedora::RelsExtDatastream) && !opts[:model_only] ) end + begin + #logger.info("PID: '#{pid}' solr_doc put into solr: #{solr_doc.inspect}") + rescue + logger.info("Error encountered trying to output solr_doc details for pid: #{pid}") + end return solr_doc end + + ########################################################################################################### + # + # This method is comparable to load_instance except it populates an object from Solr instead of Fedora. + # It is most useful for objects used in read-only displays in order to speed up loading time. If only + # a pid is passed in it will attempt to load a solr document and then populate an ActiveFedora::Base object + # based on the solr doc including any metadata datastreams and relationships. + # + # solr_doc is an optional parameter and if a value is passed it will not query solr again and just use the + # one passed to populate the object. + # + ########################################################################################################### + def self.load_instance_from_solr(pid,solr_doc=nil) + if solr_doc.nil? + result = find_by_solr(pid) + raise "Object #{pid} not found in solr" if result.nil? + solr_doc = result.hits.first + #double check pid and id in record match + raise "Object #{pid} not found in Solr" unless !result.nil? && !solr_doc.nil? && pid == solr_doc[SOLR_DOCUMENT_ID] + else + raise "Solr document record id and pid do not match" unless pid == solr_doc[SOLR_DOCUMENT_ID] + end + + create_date = solr_doc[Solrizer::FieldNameMapper.solr_name(:system_create, :date)].nil? ? solr_doc[Solrizer::FieldNameMapper.solr_name(:system_create, :date).to_s] : solr_doc[Solrizer::FieldNameMapper.solr_name(:system_create, :date)] + modified_date = solr_doc[Solrizer::FieldNameMapper.solr_name(:system_create, :date)].nil? ? solr_doc[Solrizer::FieldNameMapper.solr_name(:system_modified, :date).to_s] : solr_doc[Solrizer::FieldNameMapper.solr_name(:system_modified, :date)] + obj = self.new({:pid=>solr_doc[SOLR_DOCUMENT_ID],:create_date=>create_date,:modified_date=>modified_date}) + obj.new_object = false + #set by default to load any dependent relationship objects from solr as well + obj.load_from_solr = true + #need to call rels_ext once so it exists when iterating over datastreams + obj.rels_ext + obj.datastreams.each_value do |ds| + if ds.respond_to? (:from_solr) + ds.from_solr(solr_doc) if ds.kind_of?(ActiveFedora::MetadataDatastream) || ds.kind_of?(ActiveFedora::NokogiriDatastream) || ( ds.kind_of?(ActiveFedora::RelsExtDatastream)) + end + end + obj + end # Updates Solr index with self. def update_index if defined?( Solrizer::Solrizer ) + #logger.info("Trying to solrize pid: #{pid}") solrizer = Solrizer::Solrizer.new solrizer.solrize( self ) else + #logger.info("Trying to update solr for pid: #{pid}") SolrService.instance.conn.update(self.to_solr) end end # An ActiveRecord-ism to udpate metadata values. @@ -519,10 +801,14 @@ end return arr end end + def logger + @logger ||= defined?(RAILS_DEFAULT_LOGGER) ? RAILS_DEFAULT_LOGGER : Logger.new(STDOUT) + end + private def configure_defined_datastreams if self.class.ds_specs self.class.ds_specs.each do |name,ar| if self.datastreams.has_key?(name) @@ -562,9 +848,8 @@ end end refresh return result end - end end