Class: ActiveFedora::Base

Inherits:
Object
  • Object
show all
Includes:
SemanticNode
Defined in:
lib/active_fedora/base.rb

Overview

This class ties together many of the lower-level modules, and implements something akin to an ActiveRecord-alike interface to fedora. If you want to represent a fedora object in the ruby space, this is the class you want to extend.

The Basics

class Oralhistory < ActiveFedora::Base
   :name => "properties", :type => ActiveFedora::MetadataDatastream do |m|
    m.field "narrator",  :string
    m.field "narrator",  :text
  end
end

The above example creates a Fedora object with a metadata datastream named “properties”, which is composed of a narrator and bio field.

Datastreams defined with has_metadata are accessed via the datastreams member hash.

Direct Known Subclasses

ContentModel

Instance Attribute Summary

Attributes included from SemanticNode

#load_from_solr, #relationships_loaded, #subject

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SemanticNode

#add_relationship, #assert_kind_of, #build_statement, #clear_relationship, #conforms_to?, #ids_for_outbound, #inbound_relationship_predicates, #inbound_relationships, #load_relationships, #object_relations, #outbound_relationship_predicates, #outbound_relationships, #relationship_predicates, #relationships, #relationships_are_dirty, #relationships_are_dirty=, #relationships_desc, #remove_relationship

Constructor Details

#initialize(attrs = nil) ⇒ Base

Constructor. You may supply a custom :pid, or we call the Fedora Rest API for the next available Fedora pid, and mark as new object. Also, if attrs does not contain :pid but does contain :namespace it will pass the :namespace value to Fedora::Repository.nextid to generate the next pid available within the given namespace.



75
76
77
78
79
80
81
82
83
84
85
# File 'lib/active_fedora/base.rb', line 75

def initialize(attrs = nil)
  attrs = {} if attrs.nil?
  attributes = attrs.dup
  @inner_object = UnsavedDigitalObject.new(self.class, attributes.delete(:namespace), attributes.delete(:pid))
  self.relationships_loaded = true
  load_datastreams

  [:new_object,:create_date, :modified_date].each { |k| attributes.delete(k)}
  self.attributes=attributes
  run_callbacks :initialize
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/active_fedora/base.rb', line 32

def method_missing(name, *args)
  dsid = corresponding_datastream_name(name)
  if dsid
    ### Create and invoke a proxy method 
    self.class.send :define_method, name do
        datastreams[dsid]
    end
    self.send(name)
  else 
    super
  end
end

Class Method Details

.assign_pid(obj) ⇒ String

if you are doing sharding, override this method to do something other than use a sequence

Returns:

  • (String)

    the unique pid for a new object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/active_fedora/base.rb', line 179

def self.assign_pid(obj)
  args = {}
  args[:namespace] = obj.namespace if obj.namespace
  # TODO: This juggling of Fedora credentials & establishing connections should be handled by 
  # an establish_fedora_connection method,possibly wrap it all into a fedora_connection method - MZ 06-05-2012
  if ActiveFedora.config.sharded?
    credentials = ActiveFedora.config.credentials[0]
  else
    credentials = ActiveFedora.config.credentials
  end
  fedora_connection[0] ||= ActiveFedora::RubydoraConnection.new(credentials)
  d = REXML::Document.new( fedora_connection[0].connection.next_pid(args))
  pid =d.elements['//pid'].text
  pid
end

.connection_for_pid(pid) ⇒ Rubydora::Repository

Uses shard_index to find or create the rubydora connection for this pid

Parameters:

  • pid (String)

    the identifier of the object to get the connection for

Returns:

  • (Rubydora::Repository)

    The repository that the identifier exists in.



119
120
121
122
123
124
125
126
127
128
129
# File 'lib/active_fedora/base.rb', line 119

def self.connection_for_pid(pid)
  idx = shard_index(pid)
  unless fedora_connection.has_key? idx
    if ActiveFedora.config.sharded?
      fedora_connection[idx] = RubydoraConnection.new(ActiveFedora.config.credentials[idx])
    else
      fedora_connection[idx] = RubydoraConnection.new(ActiveFedora.config.credentials)
    end
  end
  fedora_connection[idx].connection
end

.create(args = {}) ⇒ Object



151
152
153
154
155
# File 'lib/active_fedora/base.rb', line 151

def self.create(args = {})
  obj = self.new(args)
  obj.save
  obj
end

.datastream_class_for_name(dsid) ⇒ Object



147
148
149
# File 'lib/active_fedora/base.rb', line 147

def self.datastream_class_for_name(dsid)
  ds_specs[dsid] ? ds_specs[dsid][:type] : ActiveFedora::Datastream
end

.load_instance_from_solr(pid, solr_doc = nil) ⇒ Object

This method can be used instead of ActiveFedora::Model::ClassMethods.find.

It works similarly 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 query solr for a corresponding solr document and then use it to populate this object.

If a value is passed in for optional parameter solr_doc it will not query solr again and just use the one passed to populate the object.

It will anything stored within solr such as metadata and relationships. Non-metadata datastreams will not be loaded and if needed you should use find instead.



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/active_fedora/base.rb', line 390

def self.load_instance_from_solr(pid,solr_doc=nil)
  if solr_doc.nil?
    result = find_by_solr(pid)
    raise ActiveFedora::ObjectNotFoundError, "Object #{pid} not found in solr" if result.nil?
    solr_doc = result.first
    #double check pid and id in record match
    raise ActiveFedora::ObjectNotFoundError, "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
  klass = if class_str = solr_doc['active_fedora_model_s']
    class_str.first.constantize
  else
    ActiveFedora::Base
  end

  profile_json = Array(solr_doc[ActiveFedora::Base.profile_solr_name]).first
  unless profile_json.present?
    raise ActiveFedora::ObjectNotFoundError, "Object #{pid} does not contain a solrized profile"
  end
  profile_hash = ActiveSupport::JSON.decode(profile_json)
  obj = klass.allocate.init_with(SolrDigitalObject.new(solr_doc, profile_hash, klass))
  #set by default to load any dependent relationship objects from solr as well
  #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?(:profile_from_hash) and (ds_prof = profile_hash['datastreams'][ds.dsid])
      ds.profile_from_hash(ds_prof)
    end
    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.inner_object.freeze
  obj
end

.pids_from_uris(uris) ⇒ Object



491
492
493
494
495
496
497
498
499
500
501
# File 'lib/active_fedora/base.rb', line 491

def self.pids_from_uris(uris) 
  if uris.class == String
    return uris.gsub("info:fedora/", "")
  elsif uris.class == Array
    arr = []
    uris.each do |uri|
      arr << uri.gsub("info:fedora/", "")
    end
    return arr
  end
end

.shard_index(pid) ⇒ Integer

This is where your sharding strategy is implemented – it’s how we figure out which shard an object will be (or was) written to. Given a pid, it decides which shard that pid will be written to (and thus retrieved from). For a given pid, as long as your shard configuration remains the same it will always return the same value. If you’re not using sharding, this will always return 0, meaning use the first/only Fedora Repository in your configuration. Default strategy runs a modulo of the md5 of the pid against the number of shards. If you want to use a different sharding strategy, override this method. Make sure that it will always return the same value for a given pid and shard configuration.

Returns:

  • (Integer)

    the index of the shard this object is stored in



138
139
140
141
142
143
144
# File 'lib/active_fedora/base.rb', line 138

def self.shard_index(pid)
  if ActiveFedora.config.sharded?
    Digest::MD5.hexdigest(pid).hex % ActiveFedora.config.credentials.length
  else
    0
  end
end

Instance Method Details

#==(comparison_object) ⇒ Object



284
285
286
287
288
289
# File 'lib/active_fedora/base.rb', line 284

def ==(comparison_object)
     comparison_object.equal?(self) ||
       (comparison_object.instance_of?(self.class) &&
         comparison_object.pid == pid &&
         !comparison_object.new_record?)
end

#adapt_to(klass) ⇒ Object

This method adapts the inner_object to a new ActiveFedora::Base implementation This is intended to minimize redundant interactions with Fedora



346
347
348
349
350
351
# File 'lib/active_fedora/base.rb', line 346

def adapt_to(klass)
  unless klass.ancestors.include? ActiveFedora::Base
    raise "Cannot adapt #{self.class.name} to #{klass.name}: Not a ActiveFedora::Base subclass"
  end
  klass.allocate.init_with(inner_object)
end

#adapt_to_cmodelObject

Examine the :has_model assertions in the RELS-EXT. Adapt this class to the first first known model



354
355
356
357
# File 'lib/active_fedora/base.rb', line 354

def adapt_to_cmodel
  the_model = ActiveFedora::ContentModel.known_models_for( self ).first
  self.class != the_model ? self.adapt_to(the_model) : self
end

#attributes=(properties) ⇒ Object



64
65
66
67
68
# File 'lib/active_fedora/base.rb', line 64

def attributes=(properties)
  properties.each do |k, v|
    respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
  end
end

#cloneObject



157
158
159
160
# File 'lib/active_fedora/base.rb', line 157

def clone
  new_object = self.class.create
  clone_into(new_object)
end

#clone_into(new_object) ⇒ Object

Clone the datastreams from this object into the provided object, while preserving the pid of the provided object

Parameters:

  • new_object (Base)

    clone into this object



164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/active_fedora/base.rb', line 164

def clone_into(new_object)
  rels = Nokogiri::XML( rels_ext.content)
  rels.xpath("//rdf:Description/@rdf:about").first.value = new_object.internal_uri
  new_object.rels_ext.content = rels.to_xml
  new_object.rels_ext.dirty = false

  datastreams.each do |k, v|
    next if k == 'RELS-EXT'
    new_object.datastreams[k].content = v.content
  end
  new_object if new_object.save
end

#create_dateObject

return the create_date of the inner object (unless it’s a new object)



235
236
237
# File 'lib/active_fedora/base.rb', line 235

def create_date
  @inner_object.new? ? Time.now : @inner_object.profile["objCreateDate"]
end

#fieldsObject

Return a hash of all available metadata fields for all ActiveFedora::MetadataDatastream datastreams, as well as system_create_date, system_modified_date, active_fedora_model_field, and the object id.



257
258
259
260
261
262
263
264
# File 'lib/active_fedora/base.rb', line 257

def fields
# TODO this can likely be removed once find_by_fields_by_solr is removed
  fields = {:id => {:values => [pid]}, :system_create_date => {:values => [self.create_date], :type=>:date}, :system_modified_date => {:values => [self.modified_date], :type=>:date}, :active_fedora_model => {:values => [self.class.inspect], :type=>:symbol}}
  datastreams.values.each do |ds|        
    fields.merge!(ds.fields) if [ActiveFedora::MetadataDatastream, ActiveFedora::SimpleDatastream, ActiveFedora::QualifiedDublinCoreDatastream].include?(ds.class)
  end
  return fields
end

#get_values_from_datastream(dsid, field_key, default = []) ⇒ Object



483
484
485
486
487
488
489
# File 'lib/active_fedora/base.rb', line 483

def get_values_from_datastream(dsid,field_key,default=[])
  if datastreams.include?(dsid)
    return datastreams[dsid].get_values(field_key,default)
  else
    return nil
  end
end

#idObject

Needed for the nested form helper



207
208
209
# File 'lib/active_fedora/base.rb', line 207

def id   ### Needed for the nested form helper
  self.pid
end

#init_with(inner_obj) ⇒ Object

Initialize an empty model object and set the inner_obj example:

class Post < ActiveFedora::Base
   :name => "properties", :type => ActiveFedora::MetadataDatastream
end

post = Post.allocate
post.init_with(DigitalObject.find(pid))
post.properties.title # => 'hello world'


98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/active_fedora/base.rb', line 98

def init_with(inner_obj)
  @inner_object = inner_obj
  unless @inner_object.is_a? SolrDigitalObject
    @inner_object.original_class = self.class
    ## Replace existing unchanged datastreams with the definitions found in this class if they have a different type.
    ## Any datastream that is deleted here will cause a reload from fedora, so avoid it whenever possible
    ds_specs.keys.each do |key|
      if !@inner_object.datastreams[key].changed.include?(:content) && @inner_object.datastreams[key].class != self.class.ds_specs[key][:type]
        @inner_object.datastreams.delete(key)
      end
    end
  end
  load_datastreams
  run_callbacks :find
  run_callbacks :initialize
  self
end

#inner_objectObject

:nodoc



195
196
197
# File 'lib/active_fedora/base.rb', line 195

def inner_object # :nodoc
  @inner_object
end

#inspectObject



291
292
293
# File 'lib/active_fedora/base.rb', line 291

def inspect
  "#<#{self.class}:#{self.hash} @pid=\"#{pid}\" >"
end

#internal_uriObject

return the internal fedora URI



216
217
218
# File 'lib/active_fedora/base.rb', line 216

def internal_uri
  "info:fedora/#{pid}"
end

#labelObject

return the label of the inner object (unless it’s a new object)



245
246
247
# File 'lib/active_fedora/base.rb', line 245

def label
  @inner_object.label
end

#label=(new_label) ⇒ Object



249
250
251
# File 'lib/active_fedora/base.rb', line 249

def label=(new_label)
  @inner_object.label = new_label
end

#modified_dateObject

return the modification date of the inner object (unless it’s a new object)



240
241
242
# File 'lib/active_fedora/base.rb', line 240

def modified_date
  @inner_object.new? ? Time.now : @inner_object.profile["objLastModDate"]
end

#new_object=(bool) ⇒ Object



51
52
53
# File 'lib/active_fedora/base.rb', line 51

def new_object=(bool)
  ActiveSupport::Deprecation.warn("ActiveFedora::Base.new_object= has been deprecated and nolonger has any effect. Will be removed in 5.0")
end

#new_object?Boolean

Has this object been saved?

Returns:

  • (Boolean)


47
48
49
# File 'lib/active_fedora/base.rb', line 47

def new_object?
  inner_object.new?
end

#new_record?Boolean

Required by associations

Returns:

  • (Boolean)


56
57
58
# File 'lib/active_fedora/base.rb', line 56

def new_record?
  self.new_object?
end

#owner_idObject

return the owner id



226
227
228
# File 'lib/active_fedora/base.rb', line 226

def owner_id
  @inner_object.ownerId
end

#owner_id=(owner_id) ⇒ Object



230
231
232
# File 'lib/active_fedora/base.rb', line 230

def owner_id=(owner_id)
  @inner_object.ownerId=(owner_id)
end

#persisted?Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/active_fedora/base.rb', line 60

def persisted?
  !new_object?
end

#pidObject

return the pid of the Fedora Object if there is no fedora object (loaded from solr) get the instance var TODO make inner_object a proxy that can hold the pid



202
203
204
# File 'lib/active_fedora/base.rb', line 202

def pid
   @inner_object.pid
end

#reifyObject

** EXPERIMENTAL ** This method returns a new object of the same class, with the internal SolrDigitalObject replaced with an actual DigitalObject.



362
363
364
365
366
367
# File 'lib/active_fedora/base.rb', line 362

def reify
  if self.inner_object.is_a? DigitalObject
    raise "#{self.inspect} is already a full digital object"
  end
  self.class.find self.pid
end

#reify!Object

** EXPERIMENTAL ** This method reinitializes a lightweight, loaded-from-solr object with an actual DigitalObject inside.



372
373
374
375
376
377
# File 'lib/active_fedora/base.rb', line 372

def reify!
  if self.inner_object.is_a? DigitalObject
    raise "#{self.inspect} is already a full digital object"
  end
  self.init_with DigitalObject.find(self.class,self.pid)
end

#solrize_profile(solr_doc = Hash.new) ⇒ Object

:nodoc:



316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/active_fedora/base.rb', line 316

def solrize_profile(solr_doc = Hash.new) # :nodoc:
  profile_hash = { 'datastreams' => {} }
  if inner_object.respond_to? :profile
    inner_object.profile.each_pair do |property,value|
      if property =~ /Date/
        value = Time.parse(value) unless value.is_a?(Time)
        value = value.xmlschema
      end
      profile_hash[property] = value
    end
  end
  self.datastreams.each_pair { |dsid,ds| profile_hash['datastreams'][dsid] = ds.solrize_profile }
  solr_doc[self.class.profile_solr_name] = profile_hash.to_json
end

#solrize_relationships(solr_doc = Hash.new) ⇒ Object

Serialize the datastream’s RDF relationships to solr

Parameters:

  • solr_doc (Hash) (defaults to: Hash.new)

    @deafult an empty Hash



333
334
335
336
337
338
339
340
341
# File 'lib/active_fedora/base.rb', line 333

def solrize_relationships(solr_doc = Hash.new)
  relationships.each_statement do |statement|
    predicate = RelsExtDatastream.short_predicate(statement.predicate)
    literal = statement.object.kind_of?(RDF::Literal)
    val = literal ? statement.object.value : statement.object.to_str
    ::Solrizer::Extractor.insert_solr_field_value(solr_doc, solr_name(predicate, :symbol), val )
  end
  return solr_doc
end

#stateObject

return the state of the inner object



221
222
223
# File 'lib/active_fedora/base.rb', line 221

def state 
  @inner_object.state
end

#to_keyObject



211
212
213
# File 'lib/active_fedora/base.rb', line 211

def to_key
  persisted? ? [pid] : nil
end

#to_solr(solr_doc = Hash.new, opts = {}) ⇒ Object

Return a Hash representation of this object where keys in the hash are appropriate Solr field names. If opts == true, the base object metadata and the RELS-EXT datastream will be omitted. This is mainly to support shelver, which calls .to_solr for each model an object subscribes to.

Parameters:

  • solr_doc (Hash) (defaults to: Hash.new)

    (optional) Hash to insert the fields into

  • opts (Hash) (defaults to: {})

    (optional)



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/active_fedora/base.rb', line 299

def to_solr(solr_doc = Hash.new, opts={})
  unless opts[:model_only]
    c_time = create_date
    c_time = Time.parse(c_time) unless c_time.is_a?(Time)
    m_time = modified_date
    m_time = Time.parse(m_time) unless m_time.is_a?(Time)
    solr_doc.merge!(SOLR_DOCUMENT_ID.to_sym => pid, ActiveFedora::SolrService.solr_name(:system_create, :date) => c_time.utc.xmlschema, ActiveFedora::SolrService.solr_name(:system_modified, :date) => m_time.utc.xmlschema, ActiveFedora::SolrService.solr_name(:active_fedora_model, :symbol) => self.class.inspect)
    solrize_profile(solr_doc)
  end
  datastreams.each_value do |ds|
    ds.ensure_xml_loaded if ds.respond_to? :ensure_xml_loaded  ### Can't put this in the model because it's often implemented in Solrizer::XML::TerminologyBasedSolrizer 
    solr_doc = ds.to_solr(solr_doc) if ds.kind_of?(ActiveFedora::RDFDatastream) || ds.kind_of?(ActiveFedora::NokogiriDatastream) || ds.kind_of?(ActiveFedora::MetadataDatastream)
  end
  solr_doc = solrize_relationships(solr_doc) unless opts[:model_only]
  solr_doc
end

#to_xml(xml = Nokogiri::XML::Document.parse("<xml><fields/><content/></xml>")) ⇒ Object

Returns the xml version of this object as a string.



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/active_fedora/base.rb', line 267

def to_xml(xml=Nokogiri::XML::Document.parse("<xml><fields/><content/></xml>"))
  ActiveSupport::Deprecation.warn("ActiveFedora::Base#to_xml has been deprecated and will be removed in version 5.0")
  fields_xml = xml.xpath('//fields').first
  builder = Nokogiri::XML::Builder.with(fields_xml) do |fields_xml|
    fields_xml.id_ pid
    fields_xml.system_create_date self.create_date
    fields_xml.system_modified_date self.modified_date
    fields_xml.active_fedora_model self.class.inspect
  end
  
  datastreams.each_value do |ds|  
    ds.to_xml(fields_xml) if ds.class.included_modules.include?(ActiveFedora::MetadataDatastreamHelper)
    ds.to_rels_ext if ds.kind_of?(ActiveFedora::RelsExtDatastream)
  end
  return xml.to_s
end

#update_datastream_attributes(params = {}, opts = {}) ⇒ Object

Updates the attributes for each datastream named in the params Hash

Examples:

Update the descMetadata and properties datastreams with new values

article = HydrangeaArticle.new
ds_values_hash = {
  "descMetadata"=>{ [{:person=>0}, :role]=>{"0"=>"role1", "1"=>"role2", "2"=>"role3"} },
  "properties"=>{ "notes"=>"foo" }
}
article.update_datastream_attributes( ds_values_hash )

Parameters:

  • params (Hash) (defaults to: {})

    A Hash whose keys correspond to datastream ids and whose values are appropriate Hashes to submit to update_indexed_attributes on that datastream

  • opts (Hash) (defaults to: {})

    (currently ignored.)



471
472
473
474
475
476
477
478
479
480
481
# File 'lib/active_fedora/base.rb', line 471

def update_datastream_attributes(params={}, opts={})
  result = params.dup
  params.each_pair do |dsid, ds_params| 
    if datastreams.include?(dsid)
      result[dsid] = datastreams[dsid].update_indexed_attributes(ds_params)
    else
      result.delete(dsid)
    end
  end
  return result
end

#update_indexed_attributes(params = {}, opts = {}) ⇒ Object

A convenience method for updating indexed attributes. The passed in hash must look like this :

{{:name=>{"0"=>"a","1"=>"b"}}

This will result in any datastream field of name :name having the value [a,b]

An index of -1 will insert a new value. any existing value at the relevant index will be overwritten.

As in update_attributes, this overwrites all available fields by default.

If you want to specify which datastream(s) to update, use the :datastreams argument like so:

m.update_attributes({"fubar"=>{"-1"=>"mork", "0"=>"york", "1"=>"mangle"}}, :datastreams=>"my_ds")

or

m.update_attributes({"fubar"=>{"-1"=>"mork", "0"=>"york", "1"=>"mangle"}}, :datastreams=>["my_ds", "my_other_ds"])


444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/active_fedora/base.rb', line 444

def update_indexed_attributes(params={}, opts={})
  if ds = opts[:datastreams]
    ds_array = []
    ds = [ds] unless ds.respond_to? :each
    ds.each do |dsname|
      ds_array << datastreams[dsname]
    end
  else
    ds_array = 
  end
  result = {}
  ds_array.each do |d|
    result[d.dsid] = d.update_indexed_attributes(params,opts)
  end
  return result
end