require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' module ActiveFedora module Associations extend ActiveSupport::Concern autoload :HasManyAssociation, 'active_fedora/associations/has_many_association' autoload :BelongsToAssociation, 'active_fedora/associations/belongs_to_association' autoload :HasAndBelongsToManyAssociation, 'active_fedora/associations/has_and_belongs_to_many_association' autoload :AssociationCollection, 'active_fedora/associations/association_collection' autoload :AssociationProxy, 'active_fedora/associations/association_proxy' private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) ivar = "@#{name}" if instance_variable_defined?(ivar) association = instance_variable_get(ivar) association if association.respond_to?(:loaded?) end end # Set the specified association instance. def association_instance_set(name, association) instance_variable_set("@#{name}", association) end module ClassMethods def has_many(association_id, options={}) raise "You must specify a property name for #{name}" if !options[:property] has_relationship association_id.to_s, options[:property], :inbound => true reflection = create_has_many_reflection(association_id, options) collection_accessor_methods(reflection, HasManyAssociation) end def belongs_to(association_id, options = {}) raise "You must specify a property name for #{name}" if !options[:property] has_relationship association_id.to_s, options[:property] reflection = create_belongs_to_reflection(association_id, options) association_accessor_methods(reflection, BelongsToAssociation) # association_constructor_method(:build, reflection, BelongsToAssociation) # association_constructor_method(:create, reflection, BelongsToAssociation) #configure_dependency_for_belongs_to(reflection) end # Specifies a many-to-many relationship with another class. The relatioship is written to both classes simultaneously. # # Adds the following methods for retrieval and query: # # [collection(force_reload = false)] # Returns an array of all the associated objects. # An empty array is returned if none are found. # [collection<<(object, ...)] # Adds one or more objects to the collection by creating associations in the join table # (collection.push and collection.concat are aliases to this method). # Note that this operation instantly fires update sql without waiting for the save or update call on the # parent object. # [collection.delete(object, ...)] # Removes one or more objects from the collection by removing their associations from the join table. # This does not destroy the objects. # [collection=objects] # Replaces the collection's content by deleting and adding objects as appropriate. # [collection_singular_ids] # Returns an array of the associated objects' ids. # [collection_singular_ids=ids] # Replace the collection by the objects identified by the primary keys in +ids+. # [collection.clear] # Removes every object from the collection. This does not destroy the objects. # [collection.empty?] # Returns +true+ if there are no associated objects. # [collection.size] # Returns the number of associated objects. # # (+collection+ is replaced with the symbol passed as the first argument, so # has_and_belongs_to_many :categories would add among others categories.empty?.) # # === Example # # A Developer class declares has_and_belongs_to_many :projects, which will add: # * Developer#projects # * Developer#projects<< # * Developer#projects.delete # * Developer#projects= # * Developer#project_ids # * Developer#project_ids= # * Developer#projects.clear # * Developer#projects.empty? # * Developer#projects.size # * Developer#projects.find(id) # * Developer#projects.exists?(...) # The declaration may include an options hash to specialize the behavior of the association. # # === Options # # [:class_name] # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So has_and_belongs_to_many :projects will by default be linked to the # Project class, but if the real class name is SuperProject, you'll have to specify it with this option. # [:property] # REQUIRED Specify the predicate to use when storing the relationship. # # Option examples: # has_and_belongs_to_many :projects, :property=>:works_on # has_and_belongs_to_many :nations, :class_name => "Country", :property=>:is_citizen_of def has_and_belongs_to_many(association_id, options = {}, &extension) reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) #configure_after_destroy_method_for_has_and_belongs_to_many(reflection) #add_association_callbacks(reflection.name, options) end private def create_has_many_reflection(association_id, options) create_reflection(:has_many, association_id, options, self) end def create_belongs_to_reflection(association_id, options) create_reflection(:belongs_to, association_id, options, self) end def create_has_and_belongs_to_many_reflection(association_id, options) create_reflection(:has_and_belongs_to_many, association_id, options, self) end def association_accessor_methods(reflection, association_proxy_class) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? association = association_instance_get(reflection.name) if association.nil? || force_reload association = association_proxy_class.new(self, reflection) retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload if retval.nil? and association_proxy_class == BelongsToAssociation association_instance_set(reflection.name, nil) return nil end association_instance_set(reflection.name, association) end association.target.nil? ? nil : association end redefine_method("loaded_#{reflection.name}?") do association = association_instance_get(reflection.name) association && association.loaded? end redefine_method("#{reflection.name}=") do |new_value| association = association_instance_get(reflection.name) if association.nil? || association.target != new_value association = association_proxy_class.new(self, reflection) end association.replace(new_value) association_instance_set(reflection.name, new_value.nil? ? nil : association) end redefine_method("set_#{reflection.name}_target") do |target| return if target.nil? and association_proxy_class == BelongsToAssociation association = association_proxy_class.new(self, reflection) association.target = target association_instance_set(reflection.name, association) end redefine_method("#{reflection.name}_id=") do |new_value| obj = new_value.empty? ? nil : reflection.klass.find(new_value) send("#{reflection.name}=", obj) end redefine_method("#{reflection.name}_id") do obj = send("#{reflection.name}") obj.pid if obj end end def collection_reader_method(reflection, association_proxy_class) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? association = association_instance_get(reflection.name) unless association association = association_proxy_class.new(self, reflection) association_instance_set(reflection.name, association) end association.reload if force_reload association end redefine_method("#{reflection.name.to_s.singularize}_ids") do send(reflection.name).map { |r| r.pid } end end def collection_accessor_methods(reflection, association_proxy_class, writer = true) collection_reader_method(reflection, association_proxy_class) if writer redefine_method("#{reflection.name}=") do |new_value| # Loads proxy class instance (defined in collection_reader_method) if not already loaded association = send(reflection.name) association.replace(new_value) association end redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| ids = (new_value || []).reject { |nid| nid.blank? } #TODO, like this when find() can return multiple records #send("#{reflection.name}=", reflection.klass.find(ids)) send("#{reflection.name}=", ids.collect { |id| reflection.klass.find(id)}) end end end end end end